Skip to main content

Service Boundaries and Domain-Driven Design

Understanding how to identify proper service boundaries using domain-driven design principles and bounded contexts.


Table of Contents​

  1. Defining Service Boundaries
  2. Domain-Driven Design Fundamentals
  3. Bounded Contexts
  4. Identifying Service Boundaries
  5. Context Mapping
  6. Anti-Patterns
  7. Refactoring Boundaries
  8. Practical Guidelines

Defining Service Boundaries​

What Makes a Good Boundary​

A good service boundary encapsulates a cohesive business capability with:

High Cohesion - Related functionality grouped together. Things that change together stay together.

Loose Coupling - Minimal dependencies on other services. Services can evolve independently.

Business Alignment - Boundary reflects business domain, not technical layers.

Clear Ownership - Single team owns the service and its data.

Stable Interface - API changes infrequently. Most changes are internal.

Cohesion vs Coupling​

Cohesion measures how related functionality within a service is. High cohesion means the service does one thing well. Low cohesion means unrelated functionality is bundled together.

Coupling measures dependencies between services. Loose coupling means services can change independently. Tight coupling means changes cascade across services.

Goal: High cohesion within services, loose coupling between services.

Decomposition Challenges​

The hardest part of microservices is getting boundaries right. Poor boundaries lead to:

  • Frequent cross-service changes
  • Complex coordination between teams
  • Performance issues from chatty services
  • Data consistency challenges

Better to start with larger services and split later than to start with wrong boundaries.

Evolutionary Design​

Service boundaries are not set in stone. As understanding of the domain evolves, boundaries should evolve too.

Expect to refactor boundaries based on:

  • Changing business requirements
  • Performance bottlenecks
  • Team structure changes
  • Technology constraints

Design for change rather than trying to get boundaries perfect upfront.


Domain-Driven Design Fundamentals​

Ubiquitous Language​

Establish a common vocabulary shared between technical and business teams. Use the same terms in code, conversations, and documentation.

In an e-commerce domain:

  • "Order" means the same thing to developers and business people
  • "Inventory" has a precise, shared definition
  • "Customer" vs "User" distinction is clear

Ambiguous language leads to misaligned implementations. Invest in developing ubiquitous language.

Strategic Design​

Strategic design focuses on high-level structure—identifying bounded contexts and their relationships. This is where service boundaries are defined.

Key patterns:

  • Bounded contexts (service boundaries)
  • Context maps (relationships between contexts)
  • Core domain (most valuable parts)
  • Subdomains (different areas of the business)

Tactical Design​

Tactical design focuses on modeling within a bounded context—entities, value objects, aggregates, domain events.

These patterns help structure code within services but don't directly define service boundaries.

Core, Supporting, and Generic Subdomains​

Core Domain - Provides competitive advantage. Where you differentiate from competitors. Invest heavily here.

Supporting Subdomain - Necessary but not differentiating. Build custom because off-the-shelf doesn't fit needs.

Generic Subdomain - Common functionality available off-the-shelf. Use existing solutions (authentication, payments, email).

Allocate resources accordingly. Focus best engineers on core domain. Use junior engineers or outsourcing for supporting subdomains. Buy generic subdomains.

Domain Events​

Domain events represent meaningful business occurrences:

  • OrderPlaced
  • PaymentReceived
  • InventoryReserved
  • ShipmentDispatched

Events are the language of business processes. They reveal how the domain actually works and help identify bounded contexts.


Bounded Contexts​

What Is a Bounded Context​

A bounded context is a boundary within which a particular model is defined and applicable. Outside that boundary, different models may apply.

The same term can mean different things in different contexts. "Customer" in sales context is different from "User" in support context.

Bounded contexts are natural candidates for service boundaries. Each bounded context can become a microservice.

Context Boundaries​

Boundaries define:

  • What is inside the context (models, rules, data)
  • What is outside the context (other models)
  • How context interacts with outside (APIs, events)

Clear boundaries prevent model confusion and enable independent evolution.

Example: E-Commerce Bounded Contexts​

Sales Context:

  • Concepts: Catalog, Product, ShoppingCart, Order, Payment
  • Focus: Helping customers purchase products
  • Data: Product details optimized for browsing and buying

Fulfillment Context:

  • Concepts: Shipment, Package, Delivery, Warehouse
  • Focus: Getting orders to customers
  • Data: Shipping details, warehouse locations

Inventory Context:

  • Concepts: Stock, Reservation, Reorder, Supplier
  • Focus: Managing product availability
  • Data: Stock levels, supplier information

Same "Product" appears in multiple contexts but has different attributes and behaviors in each.

Context Independence​

Bounded contexts should be as independent as possible. Changes within one context shouldn't ripple to others.

Independence enables:

  • Separate teams working on different contexts
  • Different technology choices per context
  • Independent deployment schedules
  • Localized changes and experiments

Shared Kernel​

Sometimes contexts need to share common models. Shared kernel is code shared between contexts, but it creates coupling.

Use sparingly. Shared kernels require coordination between teams for changes. Consider published language (well-defined API) instead.

Published Language​

A well-defined API or event schema that contexts use to communicate. The API is stable even as internal models evolve.

Published language enables independence. Internal changes don't affect consumers as long as API remains backward compatible.


Identifying Service Boundaries​

Event Storming​

Event storming is a workshop technique for discovering domain events and boundaries:

  1. Identify domain events (things that happen in the business)
  2. Arrange events in temporal order
  3. Identify commands that trigger events
  4. Identify aggregates (entities) that process commands
  5. Identify bounded contexts (where events naturally cluster)

Events naturally group around bounded contexts. Clusters reveal potential service boundaries.

Business Capability Mapping​

Identify distinct business capabilities:

  • User Management
  • Product Catalog
  • Order Processing
  • Inventory Management
  • Shipping and Fulfillment
  • Customer Support

Each capability is a candidate for a service. Capabilities are stable even as implementation changes.

Data Flow Analysis​

Trace how data flows through the system:

  • Where is data created?
  • How is it transformed?
  • Who consumes it?
  • What's the lifecycle?

Data that flows together should probably live together. Boundaries should minimize data flow across services.

Change Frequency Analysis​

Analyze what changes together:

  • Features that change together
  • Code that's modified together
  • Teams that work together

Things that change together should be in the same service. Boundaries should minimize coordinated changes.

Team Structure Analysis​

Consider existing or desired team structure (Conway's Law):

  • How many teams do you have?
  • What are their skills and focus areas?
  • How do teams communicate?

Service boundaries should align with team boundaries. One team per service (or one team managing multiple related services).

Performance and Scalability​

Consider performance characteristics:

  • What needs to scale independently?
  • What has different performance requirements?
  • Where are the performance bottlenecks?

Boundaries can separate parts with different scaling needs. But don't let performance drive all decisions—business alignment is more important.


Context Mapping​

Relationship Patterns​

Context mapping describes relationships between bounded contexts:

Partnership - Two contexts cooperate. Changes are coordinated. Teams work closely together.

Shared Kernel - Contexts share common code/models. Changes require mutual agreement. Highest coupling.

Customer-Supplier - Upstream context (supplier) provides API. Downstream context (customer) depends on it. Supplier's needs take priority.

Conformist - Downstream context conforms to upstream's model. No influence on upstream. Accept the model as-is.

Anti-Corruption Layer - Downstream context translates upstream's model to its own. Prevents upstream's model from polluting downstream.

Open Host Service - Context provides well-defined API for many consumers. API is stable and documented.

Published Language - Shared communication format (API schema, event format). Enables interoperability without shared models.

Separate Ways - Contexts have no relationship. Complete independence. Duplicate functionality if needed.

Context Map Visualization​

Create a visual map showing:

  • All bounded contexts
  • Relationships between them
  • Direction of dependencies
  • Type of relationship

This reveals:

  • Which contexts are central vs peripheral
  • Where coupling exists
  • Opportunities to reduce coupling
  • Clear boundaries for services

Dependency Direction​

Pay attention to dependency direction:

Upstream context - Consumed by others. Changes may impact many downstream contexts.

Downstream context - Consumes others. Dependent on upstream changes.

Minimize downstream dependencies. Prefer events over synchronous calls to invert dependencies.

Anti-Corruption Layers​

When integrating with external systems or legacy systems, use anti-corruption layers:

The layer translates between your clean domain model and the external system's model. This prevents external models from leaking into your domain.

Example: Legacy system calls users "CUST_REC". Your domain uses "Customer". Anti-corruption layer translates between them.


Anti-Patterns​

Distributed Monolith​

Services that appear independent but are tightly coupled:

  • Changes require coordinated deployments
  • Services share databases
  • Circular dependencies between services
  • Services communicate synchronously for every operation

You get all the complexity of distribution with none of the benefits of independence.

Anemic Services​

Services that are just thin wrappers around database tables (CRUD operations). No business logic. No real boundaries.

This is procedural programming with extra network calls. Not microservices.

Chatty Services​

Services that make many fine-grained calls to each other for single operations. Leads to:

  • Poor performance (network latency)
  • Complex coordination
  • Cascade failures

Indicates boundaries are wrong. Related functionality is split across services.

Wrong Decomposition Axis​

Splitting by technical layers instead of business capabilities:

  • UI service
  • Business logic service
  • Data service

This creates coupling across all services for any feature change. Boundaries should be vertical (business capabilities), not horizontal (technical layers).

Over-Fragmentation​

Too many tiny services. Results in:

  • Operational complexity
  • Difficulty understanding system flow
  • Performance issues
  • Complex deployments

Bigger services are okay. Don't split for the sake of splitting.

Shared Database​

Multiple services accessing the same database. Creates tight coupling:

  • Schema changes affect multiple services
  • No clear data ownership
  • Can't change database independently
  • Violates bounded context principle

Each service should own its data. Access through APIs, not direct database access.


Refactoring Boundaries​

When to Refactor​

Refactor boundaries when:

  • Frequent cross-service changes indicate coupling
  • Services are too large and teams can't work independently
  • Performance suffers from chatty communication
  • Business capabilities have evolved and boundaries don't match

Don't refactor prematurely. Wait for clear signals.

Splitting Services​

To split a service:

  1. Identify the boundary within the service
  2. Create new service with extracted functionality
  3. Duplicate data needed by new service
  4. Gradually move logic to new service
  5. Update callers to use new service
  6. Remove old code from original service
  7. Decommission duplicated data

Use feature flags to switch between old and new implementations safely.

Merging Services​

To merge services:

  1. Identify services to merge
  2. Combine into single service
  3. Unify data stores
  4. Update callers to use merged service
  5. Decommission old services

Merging is simpler than splitting. Don't be afraid to merge if boundaries were wrong.

Data Migration​

Moving data between services is challenging:

  1. Replicate data to new service
  2. Keep both in sync during transition
  3. Update consumers to read from new service
  4. Verify data consistency
  5. Stop sync and decommission old data

This may take weeks or months for large datasets. Plan carefully.

Zero-Downtime Migration​

For production systems, use patterns like:

  • Parallel run (both old and new implementations)
  • Dark launch (new implementation runs but results aren't used)
  • Canary deployment (gradual traffic shift)
  • Feature flags (toggle between implementations)

These enable safe migration with rollback capability.


Practical Guidelines​

Start Coarse, Refine Later​

Begin with larger services (bounded contexts). Split into smaller services when clear need arises.

It's easier to split services than merge them, but don't split prematurely. Learn your domain first.

Business Capability Focus​

Always define boundaries around business capabilities, not technical concerns:

  • Good: Order Management, Inventory, Shipping
  • Bad: Database Access, Business Logic, API Layer

Single Team Ownership​

Each service should be owned by one team. If multiple teams work on one service, consider splitting.

If one team owns multiple services, that's okay. Services can be grouped by team capability.

Autonomous Data​

Each service owns its data exclusively. No shared databases between services.

Services expose APIs for data access. Other services request data through APIs, not direct database queries.

Minimize Synchronous Calls​

Prefer asynchronous communication (events) over synchronous API calls. This reduces coupling and improves resilience.

Use synchronous calls only when immediate response is needed.

Consider Non-Functional Requirements​

Boundaries should consider:

  • Security (sensitive data isolation)
  • Compliance (data residency, audit requirements)
  • Performance (latency, throughput)
  • Scalability (independent scaling needs)

These may override pure business alignment in some cases.

Document Boundaries and Rationale​

Maintain documentation explaining:

  • Why each boundary exists
  • What belongs in each service
  • What doesn't belong and why
  • Relationships between services

This prevents drift and helps new team members understand the architecture.

Accept Imperfection​

Perfect boundaries don't exist. Every boundary involves trade-offs.

Accept that boundaries will evolve. Design for change rather than trying to achieve perfection upfront.

Use Automated Testing​

Comprehensive automated tests enable confident refactoring of boundaries:

  • Unit tests for service logic
  • Integration tests for service interactions
  • Contract tests for API compatibility
  • End-to-end tests for critical flows

Tests give confidence that refactoring preserves behavior.