Domain-Driven Design & Service Boundaries
The hardest problem in microservices isn't technology — it's where to draw the boundaries. Split too fine and you get a distributed monolith drowning in network calls. Split wrong and services constantly need coordinated changes. Domain-Driven Design (DDD) provides the conceptual toolkit to find natural service boundaries.
Bounded Contexts
The Core Concept
A Bounded Context is a boundary within which a particular domain model is consistent and meaningful. Inside a context, terms have precise definitions. Across contexts, the same word may mean different things.
Example: "Product" in Catalog means name, description, images, price. "Product" in Inventory means SKU, warehouse location, quantity on hand. Same word, different models — different bounded contexts.
Ubiquitous Language
Each bounded context has its own vocabulary — the ubiquitous language. This is critical because it prevents confusion:
| Term | In Catalog | In Inventory | In Shipping |
|---|---|---|---|
| Product | Name, images, price, description | SKU, quantity, location | Weight, dimensions, fragility |
| Customer | Browsing history, preferences | N/A (doesn't exist) | Delivery address, availability |
| Order | N/A | Reservation to decrement | Package to deliver |
Trying to create one unified "Product" model that satisfies all contexts leads to a bloated, confusing god-object.
Aggregates
Consistency Boundaries Within a Context
An Aggregate is a cluster of domain objects treated as a single unit for data changes. It has a root entity that controls access and enforces invariants.
- Order aggregate: Order (root) + LineItems + ShippingAddress. All change together atomically.
- Rule: Only modify one aggregate per transaction. Cross-aggregate consistency is eventual.
- Service boundary hint: Aggregates that always change together likely belong in the same service.
Context Mapping
How Contexts Relate
- Shared Kernel: Two contexts share a small subset of the model (risky — tight coupling)
- Customer-Supplier: Upstream context (supplier) provides data/events that downstream (customer) consumes
- Anti-Corruption Layer (ACL): A translation layer that protects your model from another context's model
- Conformist: Downstream adopts upstream's model as-is (easy but reduces autonomy)
- Published Language: A shared schema (e.g., protobuf, JSON schema) for communication
Heuristics for Finding Boundaries
Use these questions to discover natural service boundaries:
- Team ownership: Could one team own this end-to-end? (Conway's Law: system structure mirrors org structure)
- Data ownership: Is there a clear set of data that "belongs" here and nowhere else?
- Rate of change: Does this part change independently from the rest? Different release cadences suggest different services.
- Business capability: Does it map to a distinct business function? (Payments, Search, Recommendations)
- Coupling test: If I change this service, do I usually need to change another? If yes → probably same service.
Real-World Examples
🏢 Amazon — Two-Pizza Teams = Bounded Contexts
Amazon's famous organizational principle maps directly to DDD:
- Each team (small enough to feed with two pizzas, ~6-8 people) owns a service
- The service boundary = the team's bounded context = a business capability
- Teams have full autonomy: own code, own data, own deployment pipeline, own on-call
- Communication between teams mirrors communication between services — via well-defined APIs
- Result: thousands of services, each owned by a team that understands its domain deeply
🏢 Spotify — Squads Aligned with Service Boundaries
Spotify's squad model (before reorganization) demonstrated DDD principles:
- Squads: autonomous teams owning a feature area (Search, Playlist, Payments)
- Each squad owned services that mapped to their bounded context
- Tribes: groups of squads in related areas (analogous to sub-domains)
- Key learning: when squad boundaries didn't match service boundaries, coordination overhead exploded
Common Mistakes
⚠️ Boundary Anti-Patterns
- Nano-services: Too fine-grained — a "UserNameService" that just stores names. Creates massive network overhead for trivial operations.
- Splitting by technical layer: A "Database Service," "API Service," "UI Service" — this is layered architecture distributed over a network. Every feature change touches all services.
- Ignoring data coupling: Two services that constantly JOIN each other's data probably shouldn't be separate.
- Premature splitting: Extracting services before understanding the domain leads to wrong boundaries that are expensive to fix.
Interactive: Identify Bounded Contexts
Scenario: You're building a food delivery platform (like DoorDash). Drag concepts into the bounded context where they belong: