0

Bounded context — a subsystem with its own vocabulary and rules (DDD)

Advanced5 min read·eng-16-018
interview

Concept

Bounded context — in Domain-Driven Design (DDD), a subsystem or module with its own consistent vocabulary, models, and rules. Within a bounded context, terms have precise, unambiguous meanings. Different bounded contexts may use the same word to mean different things.

The problem it solves: In large systems, the word "Customer" might mean different things to different teams. In Sales, a "Customer" is a prospect being pursued. In Accounting, a "Customer" is a paying entity with invoices. In Support, a "Customer" is a ticket submitter. These are different models despite sharing a name.

Bounded context = language boundary: A context defines where a "ubiquitous language" applies. The "ubiquitous language" is the shared vocabulary between domain experts and developers within that context.

Context map: How different bounded contexts relate to each other. Common patterns:

  • Shared Kernel: Two contexts share a common model (costly but tight integration).
  • Anti-Corruption Layer (ACL): One context translates another's model to its own (avoids coupling).
  • Upstream/Downstream: One context publishes events, another consumes them.

In PHP/Laravel practice: Bounded contexts are often implemented as:

  • Namespaces: App\Sales\Customer vs App\Accounting\Customer — same term, different models.
  • Modules/packages: Separate folders or Composer packages.
  • Microservices: The ultimate bounded context separation — each service has its own DB and model.

Practical signs you need context boundaries: Two teams use the same word to mean different things. A model is getting modified for too many unrelated reasons. Changing one feature breaks another unrelated feature.

Code Example

php
<?php
// TWO BOUNDED CONTEXTS — same word "Customer" means different things

// SALES CONTEXT — Customer = prospect + opportunity tracking
namespace App\Sales;

class Customer
{
    public function __construct(
        public readonly int     $id,
        public readonly string  $companyName,
        public readonly string  $contactName,
        public string           $stage,        // 'lead', 'prospect', 'qualified', 'closed'
        public ?float           $estimatedValue,
    ) {}

    public function qualify(): void     { $this->stage = 'qualified'; }
    public function close(): void       { $this->stage = 'closed'; }
    public function disqualify(): void  { $this->stage = 'disqualified'; }
}

// BILLING CONTEXT — Customer = billing entity with invoices and payment terms
namespace App\Billing;

class Customer
{
    public function __construct(
        public readonly int    $id,
        public readonly string $legalName,
        public readonly string $vatNumber,
        public readonly string $paymentTerms, // 'net30', 'net60', 'immediate'
        public float           $outstandingBalance,
    ) {}

    public function addInvoice(Invoice $invoice): void { $this->outstandingBalance += $invoice->amount; }
    public function recordPayment(float $amount): void { $this->outstandingBalance -= $amount; }
    public function isOverdue(): bool { return $this->outstandingBalance > 0; }
}

// These are DIFFERENT classes representing "Customer" in different contexts
// App\Sales\Customer(id=1) and App\Billing\Customer(id=1) may share an ID
// but they model completely different aspects

// ANTI-CORRUPTION LAYER — Sales talks to Billing without sharing models
namespace App\Sales\Infrastructure;

class BillingContextAdapter
{
    public function createBillingCustomer(\App\Sales\Customer $salesCustomer): void
    {
        // Translate Sales vocabulary to Billing vocabulary
        app(\App\Billing\CustomerRepository::class)->create([
            'legal_name'    => $salesCustomer->companyName, // 'companyName' → 'legal_name'
            'payment_terms' => 'net30',                     // default for new customers
        ]);
        // Sales context never depends on Billing's model directly
    }
}

// MODULAR MONOLITH — Laravel module structure implementing contexts
// app/Modules/Sales/      ← bounded context
//   Models/Customer.php
//   Services/DealService.php
//   Events/DealClosed.php
//
// app/Modules/Billing/    ← bounded context
//   Models/Customer.php   ← DIFFERENT Customer model!
//   Services/InvoiceService.php
//   Listeners/CreateBillingCustomerOnDealClosed.php  ← cross-context event

// Context communication via events (loose coupling)
// When Sales closes a deal, it fires an event
event(new \App\Sales\Events\DealClosed($salesCustomer));

// Billing listens and translates (ACL pattern)
class CreateBillingCustomerOnDealClosed
{
    public function handle(\App\Sales\Events\DealClosed $event): void
    {
        // Translate into Billing context model — no shared model coupling
        \App\Billing\Customer::create([
            'legal_name'    => $event->customer->companyName,
            'sales_id'      => $event->customer->id,
            'payment_terms' => 'net30',
        ]);
    }
}