Introduction
A Bounded Context is a central pattern in DDD that defines clear boundaries within which a particular domain model applies. It's where the ubiquitous language has a specific, unambiguous meaning.
Key Concepts
- Bounded Context
- Different business processes
- Different data requirements
- Different teams or departments
Real World Context
In production PHP applications, bounded contexts helps teams build maintainable software by providing clear patterns for organizing complex business logic.
Deep Dive
Why Bounded Contexts?
Large systems can't have a single, unified model:
"Customer" in different contexts:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ SALES │ │ SHIPPING │ │ BILLING │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ Customer: │ │ Customer: │ │ Customer: │
│ - Name │ │ - Address │ │ - Payment Info │
│ - Credit Limit │ │ - Phone │ │ - Tax ID │
│ - Sales Rep │ │ - Delivery Pref │ │ - Billing Addr │
│ - Discount Tier │ │ │ │ - Credit Terms │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Each context has its own model of Customer with different attributes and behaviors.
Identifying Bounded Contexts
Look for:
- Different meanings for the same term
- Different teams or departments
- Different business processes
- Different data requirements
php<?php // Sales Context - cares about purchasing behavior namespace Sales; final class Customer { private CustomerId $id; private string $name; private Money $creditLimit; private DiscountTier $discountTier; private ?SalesRepId $assignedRep; public function canPurchase(Money $amount): bool { return $this->creditLimit->isGreaterThanOrEqual($amount); } public function getDiscountPercentage(): Percentage { return $this->discountTier->asPercentage(); } } // Shipping Context - cares about delivery namespace Shipping; final class Recipient { // Note: different name, same real-world person private RecipientId $id; private Address $shippingAddress; private PhoneNumber $contactPhone; private DeliveryInstructions $instructions; public function requiresSignature(): bool { return $this->instructions->requiresSignature(); } }
Context Mapping
Bounded Contexts must interact. Context Maps define these relationships:
Partnership
Two teams succeed or fail together:
php<?php // Both contexts evolve together namespace Sales; use Shipping\ShipmentScheduler; class OrderService { public function __construct( private ShipmentScheduler $shipping // Direct dependency OK ) {} }
Customer-Supplier
Upstream (supplier) context provides what downstream (customer) needs:
php<?php // Supplier: Inventory context provides stock info namespace Inventory; interface StockQueryService { public function getAvailableStock(ProductId $productId): Quantity; public function reserveStock(ProductId $productId, Quantity $qty): Reservation; } // Customer: Sales context uses inventory namespace Sales; class OrderService { public function __construct( private \Inventory\StockQueryService $inventory ) {} }
Anti-Corruption Layer (ACL)
Protect your domain from external/legacy models:
php<?php namespace Sales\Infrastructure\Acl; use Sales\Domain\Customer; use Sales\Domain\CustomerId; use LegacyCRM\ClientRecord; // External system class LegacyCrmCustomerAdapter implements CustomerRepository { public function __construct( private LegacyCRM\Database $legacyDb ) {} public function findById(CustomerId $id): ?Customer { // Fetch from legacy system $record = $this->legacyDb->fetchClient($id->toString()); if ($record === null) { return null; } // Translate to our domain model return $this->translateToDomain($record); } private function translateToDomain(ClientRecord $record): Customer { return new Customer( id: new CustomerId($record->CLIENT_ID), name: $record->CLIENT_NAME ?? 'Unknown', creditLimit: Money::fromCents( (int)($record->CREDIT_LIM * 100) ), // Map legacy codes to domain concepts discountTier: $this->mapDiscountCode($record->DISC_CODE) ); } private function mapDiscountCode(?string $code): DiscountTier { return match($code) { 'A' => DiscountTier::Premium, 'B' => DiscountTier::Standard, default => DiscountTier::None, }; } }
Published Language
Use a shared, documented format for integration:
php<?php // Published contract (e.g., OpenAPI, JSON Schema) namespace Contracts; final class OrderPlacedEvent { public const SCHEMA = [ 'type' => 'object', 'properties' => [ 'orderId' => ['type' => 'string', 'format' => 'uuid'], 'customerId' => ['type' => 'string', 'format' => 'uuid'], 'totalAmount' => [ 'type' => 'object', 'properties' => [ 'amount' => ['type' => 'integer'], 'currency' => ['type' => 'string'] ] ], 'occurredAt' => ['type' => 'string', 'format' => 'date-time'] ], 'required' => ['orderId', 'customerId', 'totalAmount', 'occurredAt'] ]; }
PHP Project Structure
src/
├── Sales/ # Bounded Context
│ ├── Domain/
│ │ ├── Customer.php
│ │ ├── Order.php
│ │ └── OrderRepository.php
│ ├── Application/
│ │ └── PlaceOrderService.php
│ └── Infrastructure/
│ ├── Persistence/
│ └── Acl/
│
├── Shipping/ # Bounded Context
│ ├── Domain/
│ │ ├── Recipient.php
│ │ └── Shipment.php
│ └── ...
│
├── Billing/ # Bounded Context
│ └── ...
│
└── SharedKernel/ # Shared code across contexts
├── Domain/
│ ├── Money.php
│ └── EventInterface.php
└── Infrastructure/
└── EventBus.php
Shared Kernel
Code shared between contexts—use sparingly:
php<?php namespace SharedKernel\Domain; // Shared across all contexts final readonly class Money { public function __construct( private int $cents, private Currency $currency ) {} public static function USD(int $cents): self { return new self($cents, Currency::USD); } public function add(Money $other): self { $this->ensureSameCurrency($other); return new self( $this->cents + $other->cents, $this->currency ); } // ... }
⚠️ Shared Kernel creates coupling. Keep it minimal and stable.
Common Pitfalls
- Overcomplicating simple cases - Not every part of the application needs bounded contexts. Apply it where complexity warrants the investment.
- Ignoring the ubiquitous language - Naming classes and methods without input from domain experts leads to a model that does not reflect the business.
- Mixing infrastructure concerns - Allowing framework dependencies to leak into the domain layer undermines the architectural benefits.
Best Practices
- Start from the domain - Model the business concepts first, then figure out persistence and infrastructure.
- Keep it simple - Use the simplest pattern that solves the problem. Introduce complexity only when needed.
- Collaborate with domain experts - The model should be shaped by business knowledge, not just technical preferences.
Summary
- Bounded Contexts is a fundamental concept in Domain-Driven Design
- Proper implementation leads to more maintainable and expressive code
- Always align your implementation with the ubiquitous language
- Apply these patterns where business complexity justifies the investment