Architecture Patterns

Domain-Driven Design: Bounded Contexts, Aggregates, and Context Maps

Learn strategic and tactical Domain-Driven Design, including ubiquitous language, bounded contexts, context mapping, entities, value objects, aggregates, repositories, and domain events.

DDDbounded contextubiquitous languageaggregatesdomain events

Why Domain-Driven Design?

Domain-Driven Design (DDD) is an approach for building software around the business domain instead of around tables, screens, or technical layers. It is most useful when the domain is complex, language matters, and different parts of the business use similar words with different meanings.

Key idea: DDD is not primarily about folder structure. It is about making the model reflect the business language and boundaries.


Strategic vs Tactical DDD

DDD has two major levels.

LevelFocusExamples
Strategic DDDSystem boundaries and team alignmentBounded contexts, context maps, subdomains
Tactical DDDCode-level domain model patternsEntities, value objects, aggregates, repositories

Use strategic DDD before tactical DDD. Clean entities do not help much if the service boundaries are wrong.


Ubiquitous Language

Ubiquitous language means engineers and domain experts use the same precise language in conversations, diagrams, tests, code, and APIs.

Example: E-Commerce

WordPossible Meaning
CustomerA registered buyer, a support contact, or a billing account
OrderShopping cart checkout, payment request, or warehouse fulfillment job
ProductCatalog item, sellable SKU, or inventory unit
ShipmentCarrier label, package, or delivery promise

DDD forces the team to clarify these meanings.

⚠️

Ambiguous language creates hidden coupling: If "order" means five things in one codebase, changes become risky because no one knows which meaning a method or table represents.


Bounded Contexts

A bounded context is a boundary inside which a model has one clear meaning. The same term may mean something different in another bounded context.

Bounded Context Heuristics

SignalWhat It Suggests
Different teams use different languageSeparate contexts
Data changes for different reasonsSeparate contexts
Different scaling or compliance needsSeparate contexts
Same entity has conflicting rulesSeparate contexts
Workflows are tightly coupledMaybe same context

Bounded contexts often become service boundaries, but they do not have to. A modular monolith can still have excellent bounded contexts.


Subdomains

Subdomains classify parts of the business by importance and differentiation.

Subdomain TypeMeaningEngineering Strategy
Core domainDifferentiates the businessInvest heavily, model carefully
Supporting domainNecessary but not uniqueBuild with moderate effort
Generic domainCommodity capabilityBuy or use standard tooling

For a food delivery company, dispatch optimization might be core, restaurant onboarding might be supporting, and payroll might be generic.


Context Mapping

Context maps describe relationships between bounded contexts. This matters because no context is isolated forever.

Common Context Relationships

RelationshipMeaning
PartnershipTwo teams coordinate closely and evolve together
Customer-supplierDownstream team depends on upstream team commitments
ConformistDownstream accepts upstream model as-is
Anti-corruption layerTranslate another model to protect your own
Shared kernelSmall shared model maintained jointly
Published languageStable public contract between contexts

Anti-Corruption Layer

An anti-corruption layer (ACL) protects your domain model from an external or legacy model.

The ACL translates terms, validates assumptions, and prevents legacy concepts from leaking everywhere.


Entities and Value Objects

Entity

An entity has identity that continues over time even when its attributes change.

ts
class Customer {
  constructor(
    public readonly id: CustomerId,
    public email: EmailAddress,
    public status: CustomerStatus
  ) {}
}

Value Object

A value object is defined by its values. It is usually immutable and has no separate identity.

ts
class Money {
  constructor(
    public readonly amount: number,
    public readonly currency: string
  ) {
    if (amount < 0) throw new Error("Money cannot be negative");
  }
}
ConceptIdentityMutabilityExample
EntityHas stable identityCan change over timeCustomer, Order
Value objectNo identity beyond valuesUsually immutableMoney, Address, DateRange

Aggregates

An aggregate is a cluster of domain objects that must be consistent together. The aggregate root is the only object external code should reference directly.

Aggregate Rules

  1. Protect invariants inside the aggregate.
  2. Only modify children through the aggregate root.
  3. Keep aggregates small.
  4. Reference other aggregates by ID, not object graph.
  5. Use domain events for cross-aggregate coordination.

Example Invariant

ts
class Order {
  private lines: OrderLine[] = [];
  private status: "draft" | "placed" = "draft";

  addLine(productId: string, quantity: number) {
    if (this.status !== "draft") {
      throw new Error("Cannot change a placed order");
    }
    if (quantity <= 0) {
      throw new Error("Quantity must be positive");
    }
    this.lines.push({ productId, quantity });
  }

  place() {
    if (this.lines.length === 0) {
      throw new Error("Cannot place an empty order");
    }
    this.status = "placed";
  }
}

Repositories, Services, and Domain Events

PatternResponsibility
RepositoryLoad and save aggregates
Domain serviceDomain logic that does not naturally belong to one entity
Application serviceOrchestrate use cases and transactions
Domain eventRecord something important that happened in the domain

Domain events should describe facts in the domain language: OrderPlaced, PaymentCaptured, ShipmentDelayed.


DDD and Microservices

DDD helps you find good service boundaries, but it does not require microservices.

ArchitectureHow DDD Helps
Modular monolithClear modules and boundaries inside one deployable
MicroservicesService boundaries aligned to bounded contexts
Event-driven systemsDomain events become integration signals
Legacy modernizationAnti-corruption layers reduce model leakage
💡

Start with boundaries, not deployment units: A well-designed modular monolith is usually better than poorly separated microservices.


Common DDD Mistakes

MistakeProblemBetter Approach
Anemic modelDomain rules live in services and scriptsPut invariants near the data they protect
Huge aggregateLocking and contention increaseKeep aggregate consistency boundary small
Generic repository everywhereHides domain intentUse repositories around aggregates
One model for all teamsLanguage conflicts accumulateSplit bounded contexts
DDD for CRUD-only appsAdds ceremony without benefitUse simpler patterns

What to Remember for Interviews

  1. Ubiquitous language: Code should reflect the language of the domain.
  2. Bounded contexts: A model is valid inside a boundary, not across the whole company.
  3. Context maps: Define how contexts integrate and depend on each other.
  4. Aggregates protect invariants: They are consistency boundaries, not just object graphs.
  5. DDD informs architecture: It helps choose module and service boundaries.

Practice: Model a ride-sharing domain. Identify bounded contexts such as rider, driver, trip, pricing, payment, dispatch, and support. Then draw the context map and decide where anti-corruption layers are needed.