Migrating legacy systems to DDD

You rarely get a green field. Migrating a legacy system toward DDD means carving out a model where the pain is, while the old system keeps paying the bills. Rewrites that freeze the old app for eighteen months usually lose.

Strangler fig

Put a façade in front (API gateway, reverse proxy, or a module boundary). New behavior goes to the new model; the rest delegates to the legacy. Move slice by slice (a bounded context, a use case), not layer by layer (“we rewrote the DAL first”). Cut over reads first if writes are scary; or writes first if the invariant is the point.

Anti-corruption layer (ACL)

Translate at the edge: legacy row shapes and SOAP names in, ubiquitous language out. The ACL is allowed to be ugly. The domain is not. Do not let Entity Framework entities from the monolith leak into the new core “just this once.”

Publish language, don’t share the database

If the new service still updates the old tables in place, you have not split the context — you have a distributed stored procedure. Prefer events or an explicit API the legacy already understands. Dual-write only with a plan to stop (outbox, reconciliation job, a date you will delete the old write path).

Where to start

  1. Pick the core domain slice that is changing anyway (a new product rule).
  2. Name the language with the people who own that rule.
  3. Introduce an aggregate and tests; wrap the legacy with an ACL.
  4. Measure: can this slice deploy without a freeze of the monolith?

Example

Legacy column names stop at the edge. CUST_NAME does not appear on PlaceOrder.

static PlaceOrder ToPlaceOrder(LegacyOrderRow row) => new(row.Id, row.Total);

sealed record LegacyOrderRow(Guid Id, decimal Total, string CUST_NAME);
sealed record PlaceOrder(Guid OrderId, decimal Total);

What breaks it

The new service still UPDATEs the monolith orders table. Call the legacy API, or publish one event through the outbox, and pick the date the old write is removed.

UPDATE orders SET status = $2 WHERE id = $1;

Pitfalls

  • Big-bang rewrite in a new stack “while we still add features to the old one” with no strangler.
  • Microservices extracted by team, not by model — same tables, more latency.
  • ACL that becomes the new god class because nobody moves logic inward.

Building blocks: DDD principles. Architecture moves: architecture improvements.