TechLead
Lesson 25 of 27
5 min read
Software Architecture

Bounded Contexts

Learn context mapping, relationships between bounded contexts, and integration patterns for large-scale systems

What are Bounded Contexts?

A Bounded Context is a central pattern in Domain-Driven Design. It defines the boundary within which a particular domain model applies. Inside a bounded context, every term in the ubiquitous language has a precise and unambiguous meaning. The same term can mean different things in different bounded contexts — and that is perfectly fine.

Why Bounded Contexts?

  • Linguistic clarity: "Customer" means one thing in Sales (a prospect) and another in Shipping (a delivery address)
  • Model independence: Each context can evolve its model independently without breaking others
  • Team autonomy: Different teams own different contexts and can work independently
  • Technical flexibility: Each context can choose its own technology stack, database, and architecture

Identifying Bounded Contexts

// The same concept has different models in different contexts

// Sales Context: Customer = prospect with purchase history and preferences
interface SalesCustomer {
  customerId: string;
  name: string;
  email: string;
  tier: "bronze" | "silver" | "gold" | "platinum";
  lifetimeValue: number;
  preferences: CustomerPreferences;
  purchaseHistory: PurchaseRecord[];
  assignedSalesRep: string;
}

// Shipping Context: Customer = delivery recipient
interface ShippingCustomer {
  recipientId: string;
  name: string;
  phone: string;
  address: DeliveryAddress;
  deliveryInstructions: string;
  accessCode?: string;
}

// Billing Context: Customer = billable entity
interface BillingCustomer {
  accountId: string;
  legalName: string;
  taxId: string;
  billingAddress: Address;
  paymentMethods: PaymentMethod[];
  creditLimit: number;
  outstandingBalance: number;
}

// Support Context: Customer = support ticket creator
interface SupportCustomer {
  userId: string;
  displayName: string;
  email: string;
  subscriptionPlan: string;
  openTickets: number;
  satisfactionScore: number;
}

Context Mapping

A Context Map visualizes the relationships between bounded contexts. It shows how contexts communicate, which team owns each context, and the nature of their integration. This is a strategic tool for understanding system boundaries and team dynamics.

Context Mapping Relationships

Relationship Description Example
Shared KernelTwo contexts share a common model — both teams must agree on changesShared User ID type between Auth and Billing
Customer-SupplierUpstream (supplier) provides data, downstream (customer) consumes itProduct Catalog supplies data to Order Management
ConformistDownstream adopts the upstream model as-is, with no translationSmall team uses a large platform's data model directly
Anti-Corruption LayerDownstream translates upstream's model to protect its own domainNew system integrating with legacy system
Open Host ServiceUpstream provides a well-defined public API for multiple consumersPayment service used by orders, subscriptions, refunds
Published LanguageA well-documented, shared language for integration (often paired with OHS)JSON Schema, Protocol Buffers, OpenAPI

Integration Between Contexts

// Integration via events (recommended for loose coupling)

// Order Context publishes
interface OrderPlacedIntegrationEvent {
  eventType: "order.placed";
  orderId: string;
  customerId: string;
  items: Array<{ productId: string; quantity: number; price: number }>;
  totalAmount: number;
  shippingAddress: {
    street: string;
    city: string;
    state: string;
    zipCode: string;
    country: string;
  };
  occurredAt: string;
}

// Shipping Context translates to its own model
class ShippingOrderEventHandler {
  constructor(private readonly shippingService: ShippingService) {}

  async handleOrderPlaced(event: OrderPlacedIntegrationEvent): Promise {
    // Translate from Order context model to Shipping context model
    const shippingRequest: CreateShipmentRequest = {
      externalOrderId: event.orderId,
      recipient: {
        // Only takes what Shipping needs — not the full customer model
        id: event.customerId,
        address: this.translateAddress(event.shippingAddress),
      },
      packages: this.calculatePackages(event.items),
      priority: this.determinePriority(event.totalAmount),
    };

    await this.shippingService.createShipment(shippingRequest);
  }

  private translateAddress(orderAddress: OrderPlacedIntegrationEvent["shippingAddress"]): ShippingAddress {
    return {
      line1: orderAddress.street,
      city: orderAddress.city,
      region: orderAddress.state,
      postalCode: orderAddress.zipCode,
      countryCode: this.toISO3166(orderAddress.country),
    };
  }
}

// Integration via synchronous API with Anti-Corruption Layer
class OrderToInventoryACL {
  constructor(private readonly inventoryClient: InventoryApiClient) {}

  async checkAvailability(items: OrderItem[]): Promise {
    // Translate from Order context to Inventory context
    const inventoryRequest = items.map(item => ({
      sku: item.productId,  // Order calls it productId, Inventory calls it sku
      requestedQty: item.quantity,
      warehouseId: "default",
    }));

    const inventoryResponse = await this.inventoryClient.checkStock(inventoryRequest);

    // Translate back to Order context model
    return inventoryResponse.map(inv => ({
      productId: inv.sku,
      available: inv.availableQty >= inv.requestedQty,
      availableQuantity: inv.availableQty,
      estimatedRestockDate: inv.nextReplenishment ?? null,
    }));
  }
}

Context Boundary Guidelines

  • One team per context: A bounded context should be owned by one team — shared ownership creates friction
  • Separate databases: Each bounded context should own its data — no shared databases across contexts
  • Explicit integration: Communication between contexts should be through well-defined APIs or events, never through shared code or direct database access
  • Allow duplication: It is OK to duplicate data across contexts — prefer duplication over inappropriate coupling
  • Size matters: A bounded context should be small enough for one team but large enough to be cohesive

Continue Learning