Bounded context — a subsystem with its own vocabulary and rules (DDD)
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\CustomervsApp\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
// 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',
]);
}
}