Loading content...
We've defined bounded contexts and mapped their relationships. Now comes the engineering challenge: how do we actually implement the integration between contexts? The technical approaches we choose have profound implications for system resilience, team autonomy, consistency guarantees, and operational complexity.
Integration is where the abstract concepts of DDD meet the concrete realities of distributed systems. We must navigate tradeoffs between coupling and consistency, latency and reliability, simplicity and scalability. There's no universal best approach—the right choice depends on the specific requirements and constraints of each integration point.
This page explores the full spectrum of integration patterns, from synchronous API calls to event-driven architectures, providing guidance on when to use each approach.
By the end of this page, you will understand synchronous and asynchronous integration approaches, event-driven communication between contexts, data consistency strategies across boundaries, and practical implementation patterns for real-world systems.
Integration approaches exist on a spectrum from tightly coupled to completely decoupled. Understanding this spectrum helps you choose the right approach for each integration point.
123456789101112131415161718192021222324
TIGHT COUPLING ←←←←←←←←←←←←←←←←←←←←←←←←→→→→→→→→→→→→→→→→→→→→→→→ LOOSE COUPLING ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ Shared │ → │ Synchronous │ → │ Asynchronous│ → │ Event- │ → │ Event- ││ Database │ │ API Calls │ │ API Calls │ │ Carried │ │ Sourced ││ │ │ │ │ (Queue) │ │ State │ │ (Eventual) │└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ Immediate Immediate Guaranteed Guaranteed Eventual Consistency Consistency Delivery Delivery + Consistency High Coupling Runtime Some Decoupling Data Included Maximum No Autonomy Dependency Autonomy Trade-offs:───────────────────────────────────────────────────────────────────────────────────────── Coupling Latency Resilience Consistency ComplexityShared Database Highest Lowest Lowest Strong LowestSync API High Low Low Strong* LowAsync Queue Medium Medium Medium Eventual MediumEvent-Carried Low Higher High Eventual HigherEvent-Sourced Lowest Highest Highest Eventual Highest * Strong consistency with sync APIs requires distributed transactions (usually avoided)There's no universally correct position on this spectrum. Different integration points within the same system may warrant different approaches:
Synchronous integration means one context calls another and waits for the response. This is the most intuitive approach—it works like a function call across context boundaries.
Common implementations include REST APIs, gRPC, GraphQL, or internal method calls in a modular monolith. The calling context blocks until the callee responds.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
// ═══════════════════════════════════════════════════════════════════════════// SYNCHRONOUS INTEGRATION: Order Context calls Inventory and Pricing// ═══════════════════════════════════════════════════════════════════════════ // ─────────────────────────────────────────────────────────────────────────────// ORDER CONTEXT: Application Service// ───────────────────────────────────────────────────────────────────────────── class PlaceOrderUseCase { constructor( private orderRepository: OrderRepository, private inventoryClient: InventoryContextClient, // Sync client private pricingClient: PricingContextClient, // Sync client private paymentClient: PaymentContextClient // Sync client ) {} async execute(command: PlaceOrderCommand): Promise<OrderId> { // Step 1: Validate inventory (SYNCHRONOUS call to Inventory Context) const inventoryCheck = await this.inventoryClient.checkAvailability( command.items.map(i => ({ sku: i.sku, quantity: i.quantity })) ); if (!inventoryCheck.allAvailable) { throw new InsufficientInventoryError(inventoryCheck.unavailableItems); } // Step 2: Get current prices (SYNCHRONOUS call to Pricing Context) const priceQuote = await this.pricingClient.calculateOrderPrice({ items: command.items, customerId: command.customerId, promotionCodes: command.promotionCodes }); // Step 3: Create order in our context const order = Order.create({ customerId: command.customerId, items: command.items.map((item, index) => ({ ...item, unitPrice: priceQuote.lineItems[index].price })), totalAmount: priceQuote.total }); // Step 4: Process payment (SYNCHRONOUS call to Payment Context) const paymentResult = await this.paymentClient.processPayment({ orderId: order.id, amount: priceQuote.total, paymentMethod: command.paymentMethod }); if (!paymentResult.success) { throw new PaymentFailedError(paymentResult.reason); } // Step 5: Reserve inventory (SYNCHRONOUS call to Inventory Context) await this.inventoryClient.reserveItems(order.id, command.items); // Step 6: Save and return await this.orderRepository.save(order); return order.id; }} // ─────────────────────────────────────────────────────────────────────────────// CLIENT FOR SYNCHRONOUS CALLS// ───────────────────────────────────────────────────────────────────────────── class InventoryContextClient { constructor( private httpClient: HttpClient, private acl: InventoryAntiCorruptionLayer ) {} async checkAvailability(items: ItemQuantity[]): Promise<AvailabilityResult> { try { // Call Inventory Context's REST API const response = await this.httpClient.post( 'http://inventory-service/api/v1/availability/check', { items }, { timeout: 5000 } // Timeout for resilience ); // Translate through ACL return this.acl.translateAvailabilityResponse(response.data); } catch (error) { // Handle failure gracefully if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { throw new InventoryServiceUnavailableError(); } throw error; } }}Notice the order placement requires FOUR synchronous calls (Inventory check → Pricing → Payment → Inventory reserve). If any service is down, order placement fails. System availability = Inventory × Pricing × Payment. With 99.9% availability each, combined availability is ~99.7%. With 10 services at 99.9%, system availability drops to ~99%.
When using synchronous integration, resilience patterns become essential to prevent cascading failures and maintain acceptable user experience.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
// ═══════════════════════════════════════════════════════════════════════════// CIRCUIT BREAKER: Protect against cascading failures// ═══════════════════════════════════════════════════════════════════════════ enum CircuitState { CLOSED, // Normal operation, calls go through OPEN, // Failures exceeded threshold, calls fail immediately HALF_OPEN // Testing if service recovered} class CircuitBreaker<T> { private state: CircuitState = CircuitState.CLOSED; private failureCount: number = 0; private successCount: number = 0; private lastFailureTime: number = 0; constructor( private readonly failureThreshold: number = 5, private readonly successThreshold: number = 3, private readonly resetTimeoutMs: number = 30000 ) {} async execute(operation: () => Promise<T>, fallback?: () => T): Promise<T> { // Check if we should attempt the call if (this.state === CircuitState.OPEN) { if (Date.now() - this.lastFailureTime >= this.resetTimeoutMs) { // Time to test if service recovered this.state = CircuitState.HALF_OPEN; console.log('Circuit breaker: transitioning to HALF_OPEN'); } else { // Still in cooldown, fail fast console.log('Circuit breaker: OPEN, returning fallback'); if (fallback) return fallback(); throw new CircuitOpenError('Circuit breaker is open'); } } try { const result = await operation(); this.recordSuccess(); return result; } catch (error) { this.recordFailure(); if (fallback) return fallback(); throw error; } } private recordSuccess(): void { if (this.state === CircuitState.HALF_OPEN) { this.successCount++; if (this.successCount >= this.successThreshold) { // Service appears recovered this.state = CircuitState.CLOSED; this.failureCount = 0; this.successCount = 0; console.log('Circuit breaker: transitioning to CLOSED'); } } else { this.failureCount = 0; // Reset on success } } private recordFailure(): void { this.failureCount++; this.successCount = 0; this.lastFailureTime = Date.now(); if (this.failureCount >= this.failureThreshold) { this.state = CircuitState.OPEN; console.log(`Circuit breaker: OPEN after ${this.failureCount} failures`); } }} // ─────────────────────────────────────────────────────────────────────────────// USAGE WITH FALLBACK// ───────────────────────────────────────────────────────────────────────────── class ResilientPricingClient { private circuitBreaker = new CircuitBreaker<PriceQuote>(5, 3, 30000); private priceCache: Map<string, CachedPrice> = new Map(); constructor(private httpClient: HttpClient) {} async getPrice(sku: string): Promise<Money> { return this.circuitBreaker.execute( // Primary: call pricing service async () => { const response = await this.httpClient.get( `http://pricing-service/api/v1/prices/${sku}`, { timeout: 3000 } ); // Cache for fallback this.priceCache.set(sku, { price: response.data.price, cachedAt: Date.now() }); return Money.of(response.data.price, response.data.currency); }, // Fallback: use cached price if available () => { const cached = this.priceCache.get(sku); if (cached && (Date.now() - cached.cachedAt) < 3600000) { // 1 hour console.log(`Using cached price for ${sku}`); return Money.of(cached.price, 'USD'); } throw new PriceUnavailableError(`No price available for ${sku}`); } ); }}Event-driven integration inverts the communication model. Instead of one context calling another, contexts publish events about what happened in their domain, and other contexts subscribe to events they care about. This fundamentally changes coupling dynamics.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
// ═══════════════════════════════════════════════════════════════════════════// EVENT-DRIVEN INTEGRATION: Order Context publishes, others subscribe// ═══════════════════════════════════════════════════════════════════════════ // ─────────────────────────────────────────────────────────────────────────────// INTEGRATION EVENTS (Published Language in Shared Kernel)// ───────────────────────────────────────────────────────────────────────────── interface IntegrationEvent { eventId: string; eventType: string; occurredAt: Date; correlationId: string; // For tracing across contexts} interface OrderPlacedEvent extends IntegrationEvent { eventType: 'order.placed'; orderId: string; customerId: string; items: Array<{ sku: string; quantity: number; unitPrice: number; }>; totalAmount: number; currency: string; shippingAddress: { street: string; city: string; postalCode: string; country: string; };} interface OrderCancelledEvent extends IntegrationEvent { eventType: 'order.cancelled'; orderId: string; reason: string; cancelledBy: string; // 'customer' | 'system' | 'admin'} // ─────────────────────────────────────────────────────────────────────────────// ORDER CONTEXT: Publishing Events// ───────────────────────────────────────────────────────────────────────────── class PlaceOrderUseCase { constructor( private orderRepository: OrderRepository, private eventPublisher: IntegrationEventPublisher ) {} async execute(command: PlaceOrderCommand): Promise<OrderId> { // Create order in our context const order = Order.create(command); // Save order (includes domain events) await this.orderRepository.save(order); // Publish integration event to message broker await this.eventPublisher.publish({ eventId: uuid(), eventType: 'order.placed', occurredAt: new Date(), correlationId: command.correlationId, orderId: order.id.toString(), customerId: order.customerId.toString(), items: order.items.map(item => ({ sku: item.sku, quantity: item.quantity, unitPrice: item.price.getAmount() })), totalAmount: order.totalAmount.getAmount(), currency: order.totalAmount.getCurrency(), shippingAddress: order.shippingAddress.toDTO() } as OrderPlacedEvent); return order.id; }} // ─────────────────────────────────────────────────────────────────────────────// INVENTORY CONTEXT: Subscribing to Events// ───────────────────────────────────────────────────────────────────────────── class OrderPlacedHandler implements IntegrationEventHandler<OrderPlacedEvent> { constructor( private inventoryService: InventoryService, private acl: OrderEventAntiCorruptionLayer ) {} async handle(event: OrderPlacedEvent): Promise<void> { // Translate from Order context vocabulary to Inventory context vocabulary const reservationRequest = this.acl.translateToReservation(event); // Reserve inventory in our context await this.inventoryService.createReservation(reservationRequest); // Our context's internal logic... console.log(`Inventory reserved for order ${event.orderId}`); }} // ─────────────────────────────────────────────────────────────────────────────// FULFILLMENT CONTEXT: Also subscribing to same events// ───────────────────────────────────────────────────────────────────────────── class OrderPlacedForFulfillmentHandler implements IntegrationEventHandler<OrderPlacedEvent> { constructor( private fulfillmentService: FulfillmentService, private acl: OrderEventAntiCorruptionLayer ) {} async handle(event: OrderPlacedEvent): Promise<void> { // Translate to Fulfillment context's shipment model const shipmentRequest = this.acl.translateToShipment(event); // Create pending shipment in our context await this.fulfillmentService.createPendingShipment(shipmentRequest); }} // ─────────────────────────────────────────────────────────────────────────────// BILLING CONTEXT: Also subscribing independently// ───────────────────────────────────────────────────────────────────────────── class OrderPlacedForBillingHandler implements IntegrationEventHandler<OrderPlacedEvent> { constructor(private invoiceService: InvoiceService) {} async handle(event: OrderPlacedEvent): Promise<void> { // Create invoice from order await this.invoiceService.createInvoice({ customerId: event.customerId, orderId: event.orderId, lineItems: event.items, totalAmount: event.totalAmount, currency: event.currency }); }} // ─────────────────────────────────────────────────────────────────────────────// KEY INSIGHT: Order Context publishes ONE event// THREE different contexts consume it independently// Order Context doesn't know or care about consumers// ─────────────────────────────────────────────────────────────────────────────Notice the event contains all the data consumers need (items, address, amounts). Consumers don't need to call back to the Order service for details. This 'event-carried state transfer' pattern maximizes decoupling—consumers have everything they need in the event itself.
The design of event payloads significantly impacts the flexibility and maintainability of event-driven systems. There are several approaches, each with tradeoffs:
| Pattern | Payload Content | Coupling | Use Case |
|---|---|---|---|
| Notification Event | Just IDs and event type | Requires callback | When data is large or sensitive |
| Event-Carried State Transfer | Full data snapshot | No callback needed | When consumers need autonomy |
| Delta Event | Only changed fields | Requires previous state | When tracking changes matters |
| Command-like Event | Instructions for action | Semantic coupling | When prescribing behavior |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
// ═══════════════════════════════════════════════════════════════════════════// EVENT PAYLOAD PATTERNS: Different approaches for different needs// ═══════════════════════════════════════════════════════════════════════════ // ─────────────────────────────────────────────────────────────────────────────// PATTERN 1: NOTIFICATION EVENT (Minimal payload)// ─────────────────────────────────────────────────────────────────────────────// Just tells you something happened; consumer must fetch details interface CustomerCreatedNotification { eventType: 'customer.created'; customerId: string; // Just the ID occurredAt: Date;} // Consumer must call back to get customer detailsclass NotificationEventHandler { constructor(private customerClient: CustomerServiceClient) {} async handle(event: CustomerCreatedNotification) { // Additional call required - adds latency and coupling const customer = await this.customerClient.getCustomer(event.customerId); await this.processCustomer(customer); }} // Use when:// - Data is very large (images, documents)// - Data is sensitive and shouldn't travel through message bus// - Data changes rapidly and you need latest version // ─────────────────────────────────────────────────────────────────────────────// PATTERN 2: EVENT-CARRIED STATE TRANSFER (Rich payload)// ─────────────────────────────────────────────────────────────────────────────// Contains all the data consumers typically need interface CustomerCreatedFullEvent { eventType: 'customer.created'; customerId: string; customerName: string; email: string; phone: string; address: { street: string; city: string; postalCode: string; country: string; }; createdAt: Date; tier: 'STANDARD' | 'PREMIUM' | 'VIP';} // Consumer has everything it needs - no callback requiredclass StateTransferEventHandler { async handle(event: CustomerCreatedFullEvent) { // All required data is in the event await this.createLocalCustomerProjection({ id: event.customerId, name: event.customerName, email: event.email, tier: event.tier }); }} // Use when:// - Maximum decoupling is desired// - Consumers need to work offline/autonomously// - Data is not too large to include// - Building read projections in consumers // ─────────────────────────────────────────────────────────────────────────────// PATTERN 3: DELTA EVENT (Changes only)// ─────────────────────────────────────────────────────────────────────────────// Contains only what changed, not full snapshot interface CustomerUpdatedDelta { eventType: 'customer.updated'; customerId: string; changes: { field: string; oldValue: unknown; newValue: unknown; }[]; updatedAt: Date;} // Example event:const exampleDelta: CustomerUpdatedDelta = { eventType: 'customer.updated', customerId: 'cust-123', changes: [ { field: 'email', oldValue: 'old@example.com', newValue: 'new@example.com' }, { field: 'tier', oldValue: 'STANDARD', newValue: 'PREMIUM' } ], updatedAt: new Date()}; // Use when:// - Audit trail of changes is important// - Consumers need to know what specifically changed// - Building change-data-capture pipelines // ─────────────────────────────────────────────────────────────────────────────// BEST PRACTICE: VERSION YOUR EVENTS// ───────────────────────────────────────────────────────────────────────────── interface VersionedEvent { eventType: string; version: number; // Schema version eventId: string; occurredAt: Date; payload: unknown;} // V1 of the eventinterface CustomerCreatedV1 { eventType: 'customer.created'; version: 1; customerId: string; customerName: string; email: string;} // V2 adds new field (backward compatible)interface CustomerCreatedV2 { eventType: 'customer.created'; version: 2; customerId: string; customerName: string; email: string; phone?: string; // New optional field tier: string; // New required field} // Consumer handles multiple versionsclass VersionAwareHandler { handle(event: VersionedEvent) { switch (event.version) { case 1: return this.handleV1(event as CustomerCreatedV1); case 2: return this.handleV2(event as CustomerCreatedV2); default: throw new UnknownEventVersionError(event.version); } }}With separate bounded contexts (especially with separate databases), we must accept that strong consistency across context boundaries is impractical. Instead, we design for eventual consistency and handle the temporary inconsistencies gracefully.
Distributed transactions (2PC/XA) across bounded contexts are almost always a mistake. They create tight coupling, reduce availability, and often don't work across heterogeneous systems anyway. Accept eventual consistency and design for it.
Strategies for Eventual Consistency:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
// ═══════════════════════════════════════════════════════════════════════════// OUTBOX PATTERN: Atomic local transaction + guaranteed event delivery// ═══════════════════════════════════════════════════════════════════════════ // The problem: We want to save an order AND publish an event atomically// If we publish first and save fails: consumers get event for non-existent order// If we save first and publish fails: order exists but no one knows about it // Solution: Write event to outbox table in same transaction as order// A separate process reads outbox and publishes to message broker // ─────────────────────────────────────────────────────────────────────────────// DATABASE TABLE: Outbox stores unpublished events// ───────────────────────────────────────────────────────────────────────────── /*CREATE TABLE outbox ( id UUID PRIMARY KEY, aggregate_type VARCHAR(255) NOT NULL, aggregate_id VARCHAR(255) NOT NULL, event_type VARCHAR(255) NOT NULL, payload JSONB NOT NULL, created_at TIMESTAMP NOT NULL, published_at TIMESTAMP NULL, published BOOLEAN DEFAULT FALSE); CREATE INDEX idx_outbox_unpublished ON outbox (created_at) WHERE published = FALSE;*/ // ─────────────────────────────────────────────────────────────────────────────// ORDER REPOSITORY: Saves order AND outbox event in same transaction// ───────────────────────────────────────────────────────────────────────────── class OrderRepository { constructor(private db: Database) {} async save(order: Order): Promise<void> { await this.db.transaction(async (tx) => { // Save the order await tx.query( 'INSERT INTO orders (id, customer_id, status, ...) VALUES ($1, $2, $3, ...)', [order.id, order.customerId, order.status, ...] ); // Write integration events to outbox (same transaction!) for (const event of order.getPendingIntegrationEvents()) { await tx.query( `INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, payload, created_at) VALUES ($1, $2, $3, $4, $5, $6)`, [ event.eventId, 'Order', order.id, event.eventType, JSON.stringify(event), new Date() ] ); } order.clearPendingIntegrationEvents(); }); // Both order AND events are saved atomically // Either both succeed or both fail }} // ─────────────────────────────────────────────────────────────────────────────// OUTBOX PUBLISHER: Separate process reads and publishes events// ───────────────────────────────────────────────────────────────────────────── class OutboxPublisher { constructor( private db: Database, private messageBroker: MessageBroker ) {} async publishPendingEvents(): Promise<void> { // Fetch unpublished events const events = await this.db.query( `SELECT * FROM outbox WHERE published = FALSE ORDER BY created_at LIMIT 100 FOR UPDATE SKIP LOCKED` // Allow concurrent publishers ); for (const event of events) { try { // Publish to message broker await this.messageBroker.publish( event.event_type, JSON.parse(event.payload) ); // Mark as published await this.db.query( 'UPDATE outbox SET published = TRUE, published_at = $1 WHERE id = $2', [new Date(), event.id] ); } catch (error) { // Log error, will retry on next poll console.error(`Failed to publish event ${event.id}:`, error); } } } // Run on a schedule (e.g., every 100ms) startPolling(intervalMs: number = 100): void { setInterval(() => this.publishPendingEvents(), intervalMs); }} // ─────────────────────────────────────────────────────────────────────────────// RESULT: Guaranteed at-least-once delivery// - Order and outbox event saved atomically// - Publisher retries until success// - Consumers must be idempotent (may receive duplicates)// ─────────────────────────────────────────────────────────────────────────────When a business operation spans multiple bounded contexts, we can't use traditional transactions. The Saga pattern manages multi-context operations as a sequence of local transactions, with compensating transactions to undo partial work if something fails.
There are two main saga implementation approaches:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
// ═══════════════════════════════════════════════════════════════════════════// ORCHESTRATION SAGA: Coordinated multi-context order processing// ═══════════════════════════════════════════════════════════════════════════ interface SagaStep { name: string; execute: () => Promise<void>; compensate: () => Promise<void>; // Undo action} class OrderFulfillmentSaga { private completedSteps: SagaStep[] = []; constructor( private orderId: string, private inventoryClient: InventoryClient, private paymentClient: PaymentClient, private shippingClient: ShippingClient ) {} async execute(): Promise<void> { const steps: SagaStep[] = [ { name: 'ReserveInventory', execute: () => this.inventoryClient.reserve(this.orderId), compensate: () => this.inventoryClient.releaseReservation(this.orderId) }, { name: 'ProcessPayment', execute: () => this.paymentClient.charge(this.orderId), compensate: () => this.paymentClient.refund(this.orderId) }, { name: 'ArrangeShipping', execute: () => this.shippingClient.createShipment(this.orderId), compensate: () => this.shippingClient.cancelShipment(this.orderId) }, { name: 'ConfirmInventoryDeduction', execute: () => this.inventoryClient.confirmDeduction(this.orderId), compensate: () => this.inventoryClient.reinstateStock(this.orderId) } ]; try { for (const step of steps) { console.log(`Saga: Executing step ${step.name}`); await step.execute(); this.completedSteps.push(step); } console.log('Saga: All steps completed successfully'); } catch (error) { console.error(`Saga: Step failed, starting compensation`); await this.compensate(); throw new SagaFailedError(this.orderId, error); } } private async compensate(): Promise<void> { // Execute compensations in reverse order for (const step of this.completedSteps.reverse()) { try { console.log(`Saga: Compensating step ${step.name}`); await step.compensate(); } catch (compensationError) { // Compensation failed - this is serious // Log for manual intervention console.error( `CRITICAL: Compensation failed for ${step.name}`, compensationError ); // In production: alert ops team, write to dead letter queue } } }} // ─────────────────────────────────────────────────────────────────────────────// SAGA EXECUTION TIMELINE// ─────────────────────────────────────────────────────────────────────────────// // HAPPY PATH:// 1. Reserve Inventory ✓// 2. Process Payment ✓// 3. Arrange Shipping ✓// 4. Confirm Deduction ✓// → Order Fulfilled!//// FAILURE AT STEP 3:// 1. Reserve Inventory ✓// 2. Process Payment ✓// 3. Arrange Shipping ✗ (shipping failed)// → Start compensation...// 2'. Refund Payment ✓// 1'. Release Inventory ✓// → Order Cancelled, customer not charged, inventory available//// ─────────────────────────────────────────────────────────────────────────────Compensation is not always a simple 'undo'. A refund is not the same as never charging. A cancelled shipment may leave a record. Design compensations that achieve semantic reversal - returning the business to an acceptable state - not necessarily identical reversal.
We've covered the technical patterns for making bounded contexts work together. Here are the essential takeaways:
| Requirement | Recommended Pattern |
|---|---|
| Immediate response needed | Synchronous API with fallbacks |
| Maximum decoupling | Event-carried state transfer |
| Guaranteed delivery | Outbox pattern + at-least-once |
| Multi-context transaction | Saga (choreography or orchestration) |
| High throughput, best effort | Fire-and-forget events |
| Auditing required | Event sourcing in core context |
Module Complete:
You've now completed the comprehensive study of Bounded Contexts—from understanding what they are, to defining their boundaries, mapping their relationships, and implementing their integration. These strategic patterns form the foundation for building complex systems that remain maintainable, scalable, and aligned with business domains.
Congratulations! You've mastered bounded contexts—the strategic heart of Domain-Driven Design. You understand how to decompose complex domains, define clear boundaries, map context relationships, and implement robust integration patterns. These skills are essential for designing large-scale systems that evolve gracefully over time.