Domain-Driven Design for Legacy Migration: Advanced Decomposition Strategies
An advanced guide to using domain-driven design concepts — bounded contexts, aggregates, and context mapping — to decompose legacy systems for incremental migration.
The strangler fig pattern tells you how to migrate incrementally. Domain-driven design tells you what to migrate and in what order. Teams that try to decompose a legacy system without a principled decomposition strategy end up with services whose boundaries do not align with how the business actually works — and they pay for that misalignment indefinitely. This guide covers the DDD concepts that matter most for legacy migration and how to apply them in practice.
Why Decomposition Decisions Determine Migration Outcomes
Bad service boundaries create distributed monoliths: systems where the code is split across multiple services but the services are so tightly coupled that they might as well be one. Deploying one service requires coordinating with three others. A change to a data model ripples across five services. The operational complexity of distributed systems is added without gaining any of the benefits.
The root cause is almost always incorrect boundary placement — services defined by technical layers (a "data service," a "notification service") rather than by business capability. DDD provides a vocabulary and a set of techniques for finding business capability boundaries that translate into services which can evolve independently.
Bounded Contexts: The Foundation of Decomposition
A bounded context is the most important concept in DDD for migration work. It is a linguistic and logical boundary within which a particular model is defined and applicable.
In practical terms: the word "customer" means something specific in the billing context (a payer with a payment method and an account balance) and something different in the fulfillment context (a recipient with a shipping address and delivery preferences). If both contexts use a single shared "customer" model, every change to customer data has to account for both meanings simultaneously — which is exactly the coupling that makes legacy monoliths hard to change.
Bounded contexts become service boundaries. Each context owns its data, its model, and its logic. Other contexts interact with it through defined interfaces, not shared tables.
Identifying Bounded Contexts in a Legacy Codebase
Legacy systems rarely have clean bounded contexts. They have a single database and a codebase where concepts from different business areas are intermingled. Finding the latent bounded contexts requires investigation:
- Interview domain experts (business stakeholders, not just engineers) about how they think about different parts of the system
- Look for places where the same word is used with different meanings in different parts of the codebase or in different business conversations
- Look for large tables that are used by many different parts of the system — these often represent a boundary problem that should be split
- Look for database transactions that span many tables — they reveal tightly coupled operations that may need to stay together in one context
Context Mapping: Understanding Dependencies Before You Extract
Before extracting any service, you need to understand how different bounded contexts relate to each other. A context map documents these relationships.
The relevant relationship patterns for migration work:
Shared Kernel: Two contexts share a subset of the domain model. This is usually a source of coupling to be resolved during migration, not preserved.
Customer-Supplier: One context (the supplier) provides something the other (the customer) depends on. The supplier has to consider the customer's needs when making changes. This relationship defines the sequence of extraction — extract the supplier context before the customer.
Conformist: A downstream context conforms to the model of an upstream context with no negotiation. Common in legacy systems where one module was built to consume another's data model as-is.
Anti-Corruption Layer (ACL): A translation layer that protects a new service from the model of the legacy system. When extracting a service that needs to interact with legacy code still in the monolith, an ACL prevents the legacy model from leaking into the new service.
Using the Context Map to Sequence Migration
The context map tells you the order in which services should be extracted. Extract the contexts that others depend on first, in upstream-to-downstream order. This prevents a situation where a newly extracted service depends on an interface that does not yet exist.
If two contexts have circular dependencies, that is a signal that the boundary between them is wrong — they may need to be rethought before either is extracted.
Aggregates and the Database Ownership Problem
An aggregate is a cluster of objects that are treated as a unit for data changes. Only one object in the aggregate (the aggregate root) is accessible from outside the cluster. This has direct implications for database design in a microservices architecture.
Each service should own the aggregates within its bounded context. No two services should write to the same table. This is the rule that legacy migrations most commonly violate — services are extracted but they continue reading and writing shared tables in the legacy database, which means the database is still a monolith even if the application is not.
Extracting database ownership is the most technically demanding part of a DDD-aligned migration:
- Identify which tables belong to which bounded context
- For tables shared across multiple contexts, determine which context is the authoritative owner
- Create an API through which other contexts read data from the owning context, instead of querying the shared table directly
- Build the data migration to move table ownership to the new service's database
- Remove direct database access for any context that was reading another context's tables
## Event Storming: Finding Context Boundaries Through Business Events
Event storming is a workshop technique that surfaces domain boundaries quickly. The process:
- Gather domain experts and engineers in the same room (or virtual space)
- Map all domain events — things that happen in the business — on a timeline using sticky notes
- Identify commands (what triggers the event), aggregates (what the command acts on), and bounded contexts (clusters of related events)
- Look for natural groupings and for places where different people use different language to describe the same things
An event storming session of four to six hours with the right participants often produces a more accurate picture of the bounded contexts in a system than weeks of code analysis alone.
A Practical Sequencing Heuristic
When the context map is built and the aggregates are identified, use this heuristic to sequence extraction:
- Extract contexts with no outbound dependencies on other legacy contexts first (leaf nodes in the dependency graph)
- Then extract contexts whose only dependencies are on already-extracted services
- Leave the highest-coupling core contexts for last, when the migration is most mature and the team's experience with the pattern is deepest
The sequencing discipline is as important as the boundary decisions. A migration that extracts in the wrong order creates intermediate states where extracted services depend on legacy interfaces that are themselves being migrated, which produces cascading changes and instability.
DDD-aligned migration decomposition is detailed, high-judgment work that benefits significantly from experience with both the patterns and the practice of applying them to messy real-world codebases. If your team is in the planning phase and wants help structuring the decomposition, reach out to Clixo.