~/blog/ddd-is-useful-for-complex-software-projects-where-understanding-the-domain-is-crucial-but-it-may
When DDD Fits a Project, and When It Does Not
A short note on the DDD decision: what makes a domain complex enough to be worth modelling, and why introducing it into a legacy system needs a plan.
Part 3 of this series covers the signals that say a domain is worth modelling in the first place. This note assumes you already have that answer and are asking a narrower, more practical question: you have an existing system, it was not built with DDD, and the domain underneath it has turned out to be complex enough to justify the pattern now. Adopting DDD on a greenfield project and introducing it into five-year-old production code are not the same decision, and treating them as the same one is how legacy rewrites go wrong.
This covers why a big-bang rewrite is the wrong default, the bubble context as the alternative that actually ships, what an anti-corruption layer is doing underneath that name, and the one signal that tells you a legacy carve-out is working before the whole migration is done.
Why the rewrite is usually the wrong plan#
The instinct, once a legacy domain is understood well enough to model properly, is to rewrite the system around that model. This fails for a boring, well-documented reason: a rewrite takes months to years, the old system has to keep running and changing throughout, and the new model has to track every change made to a system it has not replaced yet. By the time the rewrite is ready to cut over, the business rules it was built against have moved, and the project either ships stale or never ships. Martin Fowler's description of this as the "legacy rewrite trap" from the strangler fig pattern applies here directly: the fix is not replacing the system in one move, it is replacing it in pieces small enough to finish.
The bubble context#
Eric Evans, in the DDD reference material on legacy systems, calls the alternative a bubble context: a new bounded context, modelled properly from the business rules you now understand, that sits beside the legacy system rather than inside it. New feature work, or one specific capability pulled out of the legacy code, moves into the bubble. The legacy system keeps running everything it already runs.
// The bubble context exposes its own clean interface. It does not know about
// the legacy database schema, the legacy table names, or the legacy system's
// internal inconsistencies.
interface PolicyRepository
{
public function find(PolicyId $id): Policy;
public function save(Policy $policy): void;
}The bubble's repository implementation is where the translation happens:
// The anti-corruption layer. It reads whatever shape the legacy schema
// actually has (denormalised, inconsistently named, full of nullable columns
// that mean three different things depending on a status flag) and returns
// a Policy that the bubble's domain model can trust completely.
final class LegacyPolicyRepository implements PolicyRepository
{
public function find(PolicyId $id): Policy
{
$row = $this->legacyDb->fetchPolicyRow($id->toString());
return new Policy(
id: PolicyId::fromString($row['pol_id']),
status: PolicyStatus::fromLegacyCode($row['status_cd']),
premium: Money::fromMinorUnits($row['premium_cents'], 'USD'),
);
}
}That translation class is the anti-corruption layer. Its one job is absorbing the legacy schema's
mess so the new domain model never has to know status_cd exists. Two details matter here. First,
the translation runs in both directions: writes from the bubble back into the legacy schema need the
same layer, going the other way, or the two systems drift out of sync silently. Second, this layer is
allowed to be ugly. Its entire purpose is containing legacy inconsistency in one place instead of
letting it leak into a model that is supposed to be clean.
The signal that the carve-out is working#
Before the whole migration finishes, there is one check that tells you whether the approach is sound: can the bubble context be deployed, tested and reasoned about without opening the legacy codebase at all. If a change inside the bubble still requires reading legacy code to understand what will happen, the anti-corruption layer has a leak, and the bubble has not actually been isolated yet. That check is cheap to run early and expensive to discover is failing after six months of parallel development.
When this is not worth doing#
If the legacy system has months left before planned decommissioning, or the complex part of the domain lives in a corner nobody is actively changing, the bubble context is ceremony with no payoff. The pattern earns its cost when a specific slice of a legacy domain is both complex enough to need a real model and actively under development, which is a narrower condition than "the codebase is old and everyone dislikes it."
Previous reading#
Who should consider using DDD? (DDD series part 3) covers the decision this note assumes you have already made. Alternatives to Domain-Driven Design covers what to reach for when the answer to that decision is no.
$ related posts
· 19 min
What This Architecture Cost, Measured Rather Than ArguedNEW
What hexagonal architecture costs in Symfony 8: ports, layers and buses counted from the repository, and the thresholds below which it is not worth it. Part 6.
▸ DDD, CQRS and Hexagonal Architecture in Symfony 8 · 6#ddd#hexagonal-architecture#symfony#php#software-architecture#cqrs#refactoring19 min
· 18 min
The Application Layer, the Transaction, and a Working API
Application layer in Symfony 8: use cases on a Messenger command bus, the transaction boundary in config, optimistic locking and RFC 9457 errors. Part 2.
▸ DDD, CQRS and Hexagonal Architecture in Symfony 8 · 2#ddd#hexagonal-architecture#symfony#php#cqrs#messenger#software-architecture18 min
· 15 min
The Domain Model, and the Only Rule That Matters
DDD domain model in PHP 8.5 and Symfony 8: keep the container out of the domain, and choose Doctrine ORM or DBAL with the costs on the table. Series part 1.
▸ DDD, CQRS and Hexagonal Architecture in Symfony 8 · 1#ddd#hexagonal-architecture#symfony#php#doctrine#software-architecture15 min