Part 2 of 7

Drawing Lines in the Mud

How an AI agent applies Domain-Driven Design to codebases that have never heard of it

ClaraYet Team 9 min read View Full Series
Bounded context extraction

The hardest decision in any modernization isn't which cloud services to use. It's where to draw the service boundaries. Get it wrong and you have a distributed monolith — all the operational complexity of microservices with none of the benefits.

Event storming through static analysis

Traditional event storming puts domain experts in a room with sticky notes. For a legacy codebase with no documentation, we need a different approach. The decomposer runs event storming against the code itself, scanning for signals that something meaningful happened:

  • Database writes — INSERT, UPDATE, DELETE reveal where state changes
  • Message sends — email, queue publishes, webhook calls
  • External API calls — HTTP requests to third-party services
  • Scheduled operations — cron jobs, batch triggers
  • State mutations — application scope writes, session changes, cache invalidations

Each is a domain event candidate — static analysis identifies where state changes, but the business significance of each event requires validation. The agent clusters them by the code that produces and consumes them, and bounded contexts emerge from those clusters.

In a ColdFusion CMS, this reveals ContentCreated, UserAuthenticated, MediaUploaded, NavigationTreeModified. In a Java e-commerce app: OrderPlaced, PaymentProcessed, InventoryReserved, ShipmentCreated. The technique is language-agnostic — domain events are universal.

!Bounded Context Extraction — from coupled monolith through Strangler Fig to independent services

What bounded contexts look like in legacy code

In well-designed systems, bounded contexts align with package boundaries. In legacy systems, they almost never do. What we typically find:

God objects spanning multiple contexts. A single Application class initializing dozens of singletons from different domains. A 2,700-line facade class that routes to every library service in the framework. The agent traces which singletons are used together — that's where the boundaries are.

Inheritance chains crossing domains. Content types extending ORM classes extending schema classes. The agent identifies shared infrastructure (the ORM) versus domain-specific code (the content types).

Shared database tables. Two modules reading the same table is a strong coupling signal. Each instance is a decision point: merge the modules, or design explicit data synchronization.

Implicit event systems. Many legacy apps have rudimentary observer patterns — hooks after saves, callbacks on status changes. These are proto-events telling you the original developers were thinking about decoupling.

Decomposition patterns vs migration strategies

An important distinction: how you identify service boundaries is different from how you extract them. AWS prescriptive guidance covers both, but they're separate concerns.

Decomposition patterns help you decide where to draw the lines:

Pattern When It Fits
By Business Capability Clear business functions map to code modules
By Subdomain (DDD) Complex domain model with identifiable subdomains
By Transactions Clear transactional boundaries exist
Service per Team Conway's Law alignment is the priority

Migration strategies determine how you execute the extraction:

Strategy When It Fits
Strangler Fig Brownfield system that must stay live during migration
Branch by Abstraction Can't deploy incrementally (shared libraries, monolithic builds)
Parallel Run High-risk components needing side-by-side validation

The agent applies both: a decomposition pattern to identify the bounded contexts, and a migration strategy to extract them safely. In practice, the combination is almost always DDD (for identification) + Strangler Fig (for extraction).

Why Strangler Fig keeps winning

For the migration strategy, the agent almost always selects Strangler Fig. There are cases where other strategies are faster — greenfield rewrites when the legacy system can be taken offline, or cutover migrations when a maintenance window is available — but for brownfield systems serving production traffic, Strangler Fig dominates:

1. Legacy systems are brownfield — they're serving production traffic that can't go dark

2. Big-bang rewrites are high risk, especially with low test coverage

3. Applications have identifiable entry points that can be intercepted

4. API Gateway or a load balancer can gradually route traffic to new services

The Strangler Fig lets you extract services one at a time while the legacy system handles everything else. Traffic shifts gradually — 10% → 25% → 50% → 100% — with automatic rollback if error rates spike.

The extraction order

The decomposer sequences extractions by dependency, not complexity:

Wave 1 — Foundation services (lowest coupling). Email, scheduled tasks, file storage, caching. Self-contained, easy to validate, de-risk later waves.

Wave 2 — Identity and data layer. Authentication and database migration. Everything depends on these — they need to be stable before core extraction.

Wave 3 — Core domain. The main business logic. Most coupled, most risky. Every other service needs to be stable first.

Wave 4 — Admin and auxiliary. Internal-facing, can tolerate more downtime during transition.

The anti-corruption layer

When you extract a service, the remaining monolith still needs to talk to it. The decomposer designs an ACL for each extraction — a thin adapter translating between the legacy interface and the new service API:

Legacy Pattern ACL Approach
Session-based auth Validate JWT tokens during transition
Direct database queries Call new service API, fall back to legacy DB if unavailable*
In-process event hooks Publish to EventBridge, legacy listener forwards to old handlers
Application-scoped singletons Thin wrapper delegating to new service or local implementation

*A note on the database fallback path: writes through the ACL fallback need eventual consistency handling. The legacy DB may not reflect changes made through the new service API until sync catches up. Design for this from the start.

The circuit breaker pattern combined with fallback to legacy is critical: if a new service fails, the ACL falls back to the legacy code path. This makes the migration reversible at every step.

A limitation to acknowledge

Static code analysis alone cannot fully identify bounded contexts. The agent finds strong signals — data ownership, event flow, coupling patterns — but business semantics require domain expert input. A "Workflow" module might be a general-purpose engine or a content-publishing-specific pipeline. The code structure alone can't tell you which.

The decomposer's output is a well-informed starting point, not a final answer. Domain experts validate the proposed boundaries against business reality. The agent accelerates the discovery; humans confirm the judgment.

What the decomposer found in our sample apps

For the ColdFusion CMS, the decomposer identified six bounded contexts: Content Management, Identity & Access, Media Assets, Navigation & Routing, Email & Notifications, and Admin Console. The strongest coupling signal? A single Application.cfc initializing 47 singletons — the decomposer traced which singletons were used together and that's where the boundaries fell. The extraction order: Email first (zero inbound dependencies), then Identity (everything depends on auth), then Media and Navigation (parallel), then Content (the core domain), then Admin (internal-facing, can tolerate downtime).

Then there was the Java platform, where the real surprises were waiting.

The Java e-commerce platform was harder. What looked like 312 microservices were actually 8 bounded contexts with chatty, synchronous REST calls between them. The decomposer's biggest finding: 40+ database tables shared across services, meaning the "microservices" were a distributed monolith. Its recommendation: consolidate first, then re-extract with proper data ownership. A counterintuitive output — sometimes the path to microservices starts by going backward.

The .NET enterprise portal was the most straightforward decomposition — the original developers had used proper namespaces that mapped closely to business capabilities. The decomposer confirmed 5 bounded contexts: Portal, Reporting, UserManagement, Workflow, and Integration. It flagged the Windows-specific dependencies from the discovery report as cross-cutting concerns that needed platform-level solutions rather than per-context extraction. The extraction order prioritized replacing the COM-based PDF generation and NTLM auth first, since every other service depended on them.

The pattern that keeps emerging

Across the legacy codebases we've analyzed, the bounded contexts are typically already there. They're just hidden under layers of coupling. Legacy developers organized code into packages that roughly correspond to domain boundaries. Over years of maintenance, shortcuts created cross-cutting dependencies that blur those boundaries.

The decomposer's job isn't to invent boundaries. It's to find the ones that already exist and identify the coupling that needs to be broken.

Next: Part 3 — Designing the Target — where the solution architect maps these bounded contexts to specific AWS services, applies cloud design patterns, and produces an architecture with CDK construct names. The extraction order defined here becomes the migration wave plan.

"Organizations don't need more AI tools —
they need a guide."

Ready for clarity?

Let's talk about where AI can make the biggest impact in your organization — and how to actually get there.

You haven't tried Clara. Yet.