From a Big Ball of Mud to DDD
A Migration Field Guide
A step-by-step guide for migrating a real legacy monolith to Domain-Driven Design. Not theory -- practice. Not greenfield -- brownfield. Not a rewrite -- an incremental migration that never stops production. Written for .NET architects and senior developers who inherited a Big Ball of Mud and need a way out.
This series follows SubscriptionHub, a 6-year-old SaaS billing platform with 4 teams, 80K lines, circular dependencies, God Services, and 12 tests (6 of which are [Ignore]d). We diagnose its pathologies, discover the domain hiding inside it through Event Storming, and migrate it phase by phase -- tests first, always.
Table of Contents
Part I: The Disease
What is a Big Ball of Mud? How does it grow? Meet SubscriptionHub -- a SaaS billing platform with 4 teams, 7 projects, circular dependencies, implicit boundaries that nobody formalized, and ad-hoc ACLs that leak. The system that will serve as our migration case study.
Part II: The Six Pathologies
Diagnosis before treatment. The God Service that does everything. Anemic domain models. Business rules scattered across 3 implementations. Infrastructure coupling that makes testing impossible. Leaky boundaries and broken ACLs. No aggregate boundaries in a 31-entity flat DbContext. Feel the pain before seeing the cure.
Part III: Discovering Boundaries and Internals
Event Storming applied to legacy code. Big Picture Event Storming discovers four bounded contexts from the God Service's method body. Design-Level Event Storming reveals what lives inside each context -- aggregates, commands, policies, read models. The same concept means different things in different contexts. The output is the migration roadmap.
Part IV: The Migration Strategy
The Strangler Fig pattern. Why big-bang rewrites fail. Six phases, each independently deployable, each delivering value. Tests first. Bounded context libraries second. ACLs third. Value Objects, Aggregates, and Domain Events fill the structure. How to prioritize which context to migrate first.
Part V: Tests First
Before touching ANY code, write characterization tests. The golden master pattern. The testing strategy that accompanies every subsequent phase -- TDD for Value Objects and Aggregates, contract tests for ACLs, integration tests for Events. Test infrastructure: Testcontainers, WebApplicationFactory, fakes over mocks, test builders, architecture tests, CI pipeline integration.
Part VI: Create Bounded Context Libraries
Phase 2 -- the foundational structural move. Reify the boundaries discovered in Event Storming into project structure. Domain/Application/Infrastructure layers per context. Separate DbContexts, same database. Kill the Common project. SharedKernel with zero-dependency domain libs. The compiler enforces the architecture.
Part VII: Fix & Formalize ACLs
Phase 3 -- stop the bleeding at the boundaries. Fix the PaymentGateway that leaks Stripe types. Give NotificationService its own read models instead of querying the main database. Wrap the Tax API behind a domain interface. ACL anatomy, contract testing, and the three shapes of Anti-Corruption Layers.
Part VIII: Extract Value Objects
Phase 4 -- TDD. Write the test, then extract the Value Object. Money, SubscriptionPeriod, EmailAddress, TaxRate, PlanTier. Three proration implementations collapse to one method. EF Core owned entity mapping with zero schema changes. The safest refactoring with the highest ROI.
Part IX: Build Aggregates
Phase 5 -- TDD. Invariant tests first, then enforce in the aggregate. Transform anemic entities into rich domain models. The Subscription aggregate with typed IDs, private setters, Result returns, and domain events. The Strangler Fig ACL with feature flags -- old and new code coexist, rollback is a flag flip.
Part X: Introduce Domain Events
Phase 6 -- break circular dependencies with events. PlanChangedEvent, PaymentFailedEvent, InvoiceGeneratedEvent. Event handlers that live in their own context. The subscription lifecycle as a state machine. Transactional Outbox for reliable cross-context communication.
Part XI: The Final State
Before and after -- the full transformation. Comparison table across 10 dimensions. Practical lessons from the migration. The bridge to code generation: [Aggregate] and [AggregateRoot] attributes that make the compiler enforce boundaries permanently. "Start tomorrow."
How to Read This Series
If you're in pain now: start with Part II (Pathologies) to see if your symptoms match, then Part IV (Strategy) for the roadmap.
If you want the full journey: read sequentially. Each part builds on the previous one.
If you're a testing advocate: start with Part V (Tests First) -- it's the foundation for everything that follows.
If you're an architect: Parts III (Event Storming) and VI (Bounded Context Libraries) are the strategic core.
Prerequisites
- C# / .NET experience (examples use .NET 8, C# 12)
- Familiarity with EF Core
- Basic DDD vocabulary is helpful -- for a full treatment, see Domain-Driven Design & Code Generation
- Experience with legacy codebases (you know the pain -- that's why you're here)