Teams usually adopt microservices to move faster: independent deploys, independent scaling, and independent ownership. Many end up with the opposite, a distributed monolith. Every feature touches five services, deploys must be coordinated, and one slow service drags down every request.
The technology is rarely the cause. The cause is almost always where the lines were drawn. Domain-Driven Design (DDD) offers a vocabulary and a set of tools for drawing them well.
The wrong ways to split
Before looking at what works, it helps to recognize the common mistakes:
- By technical layer: an "API service", a "business logic service", and a "database service". Every feature crosses every layer, so every feature touches every service.
- By data entity: a
user-service, anorder-service, and aproduct-service, each a thin CRUD wrapper over one table. Business workflows then become chatty chains of remote calls. - By org chart of the moment: teams reorganize more often than domains change.
Each of these creates services that must change together, and needing to change together is the definition of coupling.
Bounded contexts: the core idea
A bounded context is a part of the business where one model and one vocabulary apply consistently. The same word can mean different things in different contexts, and that's fine.
Take "product" in an e-commerce company:
- In Catalog, a product has descriptions, images, categories, and SEO metadata.
- In Pricing, a product is a SKU with base prices, discounts, and currency rules.
- In Inventory, a product is a stock-keeping unit with quantities per warehouse.
- In Shipping, a product is a parcel with weight and dimensions.
Forcing all of that into one shared Product model creates a giant object that every team fights over. Letting each context keep its own model, linked by a shared ID, lets each one change on its own schedule.
Bounded contexts are the best starting candidates for service boundaries.
Discovering contexts with event storming
You can't find boundaries by staring at a database schema. You find them by understanding how the business works. Event storming is a workshop technique that makes this concrete:
- Put engineers and domain experts in a room (physical or virtual) with a long wall.
- Write domain events on sticky notes in past tense:
OrderPlaced,PaymentCaptured,ItemsReserved,ShipmentDispatched. - Arrange them on a timeline.
- Add the commands that trigger them and the actors who issue those commands.
- Watch for clusters, and for places where the language changes.
Where vocabulary shifts, where different people own decisions, and where there are natural handoffs, you've probably found a context boundary.
Aggregates: the unit of consistency
Inside a context, an aggregate is a cluster of objects that must stay consistent together, with one root entity that guards its invariants. An Order aggregate might contain line items and enforce "total must equal the sum of lines" and "can't add items after checkout".
Rules that matter for service design:
- One transaction modifies one aggregate. If a use case needs to update two aggregates atomically, either they belong together or the requirement should become eventual consistency.
- Aggregates reference each other by ID, not by object reference.
- Keep aggregates small. Large aggregates mean lock contention and bloated loads.
A service usually owns one or several related aggregates. It should almost never share an aggregate with another service.
Practical tests for a proposed boundary
Once you have candidate services, stress-test them with these questions:
1. The change test
Take the last 20 feature requests. How many would touch more than one of your proposed services? If most would, the boundaries are wrong.
2. The data ownership test
Can you name the single service that writes each table? Shared write access to a table is a hidden contract that makes independent deploys impossible.
3. The chattiness test
Does a typical request need synchronous calls across three or more services? Long synchronous chains multiply latency and failure probability. If service A calls service B for every operation, they may belong together.
4. The team test
Can one team own the service end to end, including code, on-call, and roadmap? Conway's Law applies whether you plan for it or not. A service owned by three teams is owned by nobody.
5. The language test
Do the service's API names match the words domain experts use? A mismatch often means the boundary is cutting through the middle of a concept.
Integrating contexts without coupling them
Contexts still need to cooperate. DDD names several integration patterns:
- Published events.
OrderingpublishesOrderPlaced, andInventoryandNotificationsreact. The publisher doesn't know who is listening. - Anti-corruption layer (ACL). When you consume another context's model, especially a legacy system's, translate it into your own model at the edge so foreign concepts don't leak into your domain.
- Open host service. A context exposes a stable, well-documented API designed for multiple consumers, instead of ad-hoc endpoints for each caller.
Each context keeps its own data. When Shipping needs a customer's address, it keeps its own copy, filled from events, instead of querying the Customer service on every request.
Start with a modular monolith
Boundaries are hard to get right on the first attempt, and moving a boundary between two deployed services is expensive. Moving it between two modules in one codebase is a refactor.
A strong approach is to build a modular monolith first:
- One deployable unit, with modules that match your bounded contexts.
- Modules communicate only through explicit interfaces or in-process events, never by reaching into each other's tables.
- Enforce the rules with tooling, such as import linters and schema-per-module.
When a module has a clear reason to become independent (a different scaling profile, a different release cadence, a separate team), extract it. Because its boundary has already been tested in production, extraction becomes mostly mechanical.
Warning signs you've drawn it wrong
- Releases need a deployment order across several services.
- A "shared library" contains domain models used by many services.
- Services read each other's databases "just for reporting".
- Most incidents involve cascading timeouts across a call chain.
- Nobody can explain in one sentence what a service is responsible for.
Summary
Good microservice boundaries follow the domain, not the database or the org chart. Find bounded contexts through collaboration with domain experts, keep aggregates small and transactional, integrate through events and translation layers, and treat your first split as a hypothesis. A modular monolith is the cheapest place to test that hypothesis.
