DevNotes
Support
Microservices Fundamentals · Chapter 02

Decomposing into Services & Domain-Driven Design

Splitting by business capability, bounded contexts and ubiquitous language, aggregates, database per service, sizing services, coupling and cohesion, and the anti-patterns that create a distributed monolith.

◷ 5 min read§ 10 sections? 5 interview questions{ } 0 code examples

#Introduction

The hardest part of microservices isn't Spring Cloud — it's deciding where the boundaries go. Good boundaries let teams work independently; bad ones create chatty services that must change and deploy together. Domain-Driven Design (DDD) gives the vocabulary for drawing them.

#Decompose by Business Capability

A business capability is something the business does to create value: take orders, process payments, manage inventory, ship parcels, notify customers.

  • E-commerce business
    • Catalogueproducts, categories, search
    • Orderingcarts, orders, checkout
    • Paymentscharges, refunds
    • Inventorystock levels, reservations
    • Shippingshipments, tracking
    • Customerprofiles, addresses
    • Notificationsemail, SMS, push

Each capability is a candidate service. Capabilities change slowly, so service boundaries stay stable even as features change.

#Bounded Contexts

In DDD, a bounded context is a boundary within which a model and its terms have one precise meaning. The same word can mean different things in different contexts:

Context"Product" means…
CatalogueName, description, images, categories
InventorySKU, warehouse location, quantity on hand
PricingBase price, discounts, tax rules
ShippingWeight, dimensions, hazardous flag

Forcing one giant shared Product model on everyone couples all teams. Instead, each bounded context has its own model of a product, linked by a shared ID. A bounded context is usually the right size for one service (or a small group of services owned by one team).

Ubiquitous Language

Inside a bounded context, developers and domain experts use the same terms in conversations, code and documentation — Order.place(), Shipment.dispatch() — so the code reflects how the business actually talks.

#Aggregates

An aggregate is a cluster of objects treated as one unit for changes, with one aggregate root that guards its rules:

  • Order — aggregate root
    • OrderLine
    • OrderLine
    • ShippingAddress — value object
  • Outside code changes an order only through the root (order.addLine(...), order.cancel()), which enforces invariants such as "a shipped order can't be cancelled".
  • One transaction changes one aggregate. Changes to other aggregates — especially in other services — happen through events.
  • Aggregates reference each other by ID, not by object: an Order holds a customerId, not a Customer.

#Database per Service

Each service owns its data. No other service reads or writes its tables:

Order service
orders DBonly the order service touches it
Payment service
payments DB
Inventory service
inventory DB
Shared databaseDatabase per service
Easy joins and transactionsNo cross-service joins or transactions
A schema change can break several servicesEach team evolves its schema freely
Services coupled at the data levelServices coupled only through APIs and events
One database bottleneckEach service picks and scales its own store

How do services get data they don't own?

  • Ask for it — call the owning service's API.
  • Keep a local copy — subscribe to the owner's events and store the fields you need (e.g. the order service keeps customer names from CustomerUpdated events).
  • Compose it — a gateway or "backend for frontend" calls several services and merges the results.

Cross-service workflows use sagas instead of distributed transactions — see Data Management in Microservices and the Saga pattern.

#Sizing Services

Too bigToo small ("nanoservices")
Several teams share it; deployments need coordinationEvery feature touches several services
Mixed responsibilities and scaling needsChatty network calls for simple operations
Becoming a monolith againOperational overhead per tiny service

Heuristics:

  • One bounded context per service as a starting point.
  • One team can own it fully.
  • Things that change together stay together (high cohesion).
  • A typical user request is handled by one or a few services, not ten.

#Coupling and Cohesion

Aim forAvoid
High cohesion — related behaviour and data in the same serviceLogic for one capability scattered across services
Loose coupling — services interact through stable, versioned contractsSharing databases, internal classes or deployment schedules
Asynchronous events where possibleLong chains of synchronous calls

#Anti-Patterns

  • Shared database — services coupled through tables.
  • Distributed monolith — services that must be released together.
  • Chatty services — dozens of calls per request.
  • Entity services — a "Customer service" that is only CRUD over one table, called by everyone.
  • Shared domain library — one JAR of domain classes used by all services; every change forces everyone to upgrade. (Sharing small technical libraries — logging, security config — is fine.)

#Interview Questions

Q1How do you decide service boundaries?

Decompose by business capability and DDD bounded contexts: group behaviour and data that change together, keep each service ownable by one team, and make sure a typical request touches few services.

Q2What is a bounded context?

A DDD boundary within which a domain model and its terms have one consistent meaning. Different contexts may model the same concept (like "Product") differently.

Q3What is an aggregate?

A cluster of domain objects changed as one unit through an aggregate root that enforces invariants; one transaction modifies one aggregate, and aggregates refer to each other by ID.

Q4Why should each service have its own database?

So services are loosely coupled: each team can change its schema and choose its storage independently, and no service can break another by touching its tables.

Q5How does a service get data owned by another service?

By calling its API, by maintaining a local read copy updated from the owner's events, or through API composition at a gateway or BFF.

#Key Takeaways

  • Draw boundaries around business capabilities and bounded contexts, not technical layers.
  • Each bounded context has its own model and ubiquitous language.
  • Aggregates define transactional consistency boundaries; reference other aggregates by ID.
  • Database per service is essential for independence.
  • Aim for high cohesion and loose coupling; avoid chatty, entity-only and shared-database services.