Finding Service Boundaries with DDD

The hardest part of microservices is deciding where to cut. Learn bounded contexts, ubiquitous language, aggregates and context maps from Domain-Driven Design.

Intermediate⏱ 5 min readLesson 2 of 8#microservices#ddd#bounded-context#architecture

The big idea

The word "Product" means different things in different departments of a shop:

  • Catalogue team: name, photos, description, categories.
  • Warehouse: weight, shelf location, stock count.
  • Billing: price, tax class, discounts.

If one giant Product class tries to serve everyone, every team edits it, it grows to 80 fields, and any change risks breaking someone else.

Domain-Driven Design (DDD) says: let each department have its own model of a product, inside its own boundary. That boundary is a bounded context, and it's the best guide for where to draw service boundaries.

The same "Product" means different things in different bounded contextsThe same "Product" means different things in different bounded contexts

🏛️ This lesson applies DDD to service boundaries. For the full picture, see DDD strategic design and DDD tactical patterns in the Software Architecture topic.

Key DDD vocabulary

TermMeaningExample
DomainThe business area you're building forOnline retail
SubdomainA distinct part of the businessCatalogue, ordering, shipping, billing
Bounded contextA boundary inside which one model and one language applyThe Shipping context
Ubiquitous languageWords that developers and business experts share inside a context"Shipment", "parcel", "carrier"
AggregateA cluster of objects changed together as one unit, with one rootOrder + its OrderLines
Domain eventSomething that happened that others care aboutOrderPlaced, PaymentFailed
Context mapHow bounded contexts relate to each otherOrdering → Shipping (events)

Subdomains: where to invest

Drawing diagram…

Put your best engineers and cleanest design into the core; buy generic pieces (Auth0, Stripe, SendGrid) instead of building them.

From contexts to services

Drawing diagram…

A bounded context usually becomes one service (or a small group of services owned by one team). Each context:

  • Has its own database: nobody else reads its tables.
  • Shares data only through APIs and events.
  • Keeps only the data it needs: Ordering stores the product ID, name and price at the time of ordering, not the full catalogue entry.

Aggregates: consistency boundaries

An aggregate is a group of objects that must stay consistent together, changed in one transaction through one entry point, the aggregate root.

// Order is the aggregate root: outside code never edits OrderLines directly
class Order {
  #lines = [];
  #status = "draft";

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

  submit() {
    if (this.#lines.length === 0) throw new Error("Order is empty");
    this.#status = "submitted";
    return { type: "OrderSubmitted", orderId: this.id, totalCents: this.totalCents }; // domain event
  }

  get totalCents() {
    return this.#lines.reduce((sum, l) => sum + l.quantity * l.unitPriceCents, 0);
  }
}
Drawing diagram…

Aggregate rules of thumb:

  • Keep aggregates small.
  • Reference other aggregates by ID, not by object.
  • One transaction = one aggregate. Changes across aggregates (or services) happen through events: eventual consistency.

Context mapping: how contexts relate

RelationshipMeaning
Customer / SupplierDownstream team's needs influence the upstream team's API
ConformistDownstream just accepts the upstream model as it is
Anti-Corruption Layer (ACL)Downstream translates the upstream model into its own, protecting itself from a messy or legacy model
Published languageA shared, documented format (for example, event schemas)
Shared kernelA small, jointly owned shared model (use sparingly)
Drawing diagram…

Practical heuristics for cutting services

  1. Follow business capabilities, not technical layers. ✅ "Payments service". ❌ "Database service", "Validation service".
  2. High cohesion inside, loose coupling outside: things that change together stay together.
  3. Look at the language: where the meaning of a word changes, there's probably a boundary.
  4. Check the chattiness: if two services call each other for every request, they're probably one context.
  5. Align with teams: one team should own a context end to end.
  6. Event storming: put business experts and developers in a room with sticky notes for every domain event (OrderPlaced, ItemShipped), then group them. The clusters reveal the contexts.
Drawing diagram…

Colour groups from event storming: Ordering (blue), Payments (yellow), Fulfilment (green).

Key takeaways

  • One word can mean different things in different parts of a business; give each part its own model.
  • A bounded context is the natural boundary for a microservice, with its own data and language.
  • Aggregates are consistency boundaries: small, changed in one transaction, referenced by ID.
  • Contexts communicate through APIs and domain events; protect clean models with an anti-corruption layer.
  • Cut by business capability, not technical layer. Event storming helps find the seams.