Monolith to Microservices: Finding Your Bounded Contexts
Use dependency graph analysis to identify natural service boundaries instead of guessing
The most expensive architectural decision you'll make isn't choosing microservices — it's where you draw the boundaries.
Split too early and you create a distributed monolith — all the complexity of microservices, none of the benefits. Split incorrectly and you'll spend years migrating transactions across network calls, debugging cascade failures, and retrofitting consistency models you didn't plan for.
Most teams approach service boundaries through domain modeling workshops, educated guesses, or by copying architectures from companies that look successful. These approaches can work. But they're subjective. They depend heavily on institutional knowledge and intuition that new team members don't have.
There's a better starting point: let your code tell you where the boundaries are.
Your monolith has already evolved natural fault lines based on how your team actually works. Dependency graph analysis makes those boundaries visible.
The Problem with Traditional Approaches
Before diving into the data-driven approach, it's worth acknowledging why traditional methods often fall short.
Domain-Driven Design Workshops
DDD is powerful, but:
- Requires deep domain expertise across the team
- Reflects how the business should work, not necessarily how the code actually evolved
- Misses technical dependencies that don't map cleanly to domain concepts
- Can produce beautiful diagrams that don't survive contact with production reality
Team Topology
Conway's Law suggests organizing services around team boundaries:
- Works well if teams are already structured cleanly
- Breaks down in organizations with historical reorganizations
- Doesn't account for shared infrastructure or technical coupling
- Can perpetuate existing organizational problems
Copying Netflix, Amazon, etc.
Following proven patterns is smart — but:
- Those patterns emerged from constraints you likely don't have
- Their domains are different
- They had years to iterate; you need to get it right faster
- Survivor bias hides the experiments that failed
Architecture by imitation rarely fits perfectly.
The Data-Driven Alternative: Cluster Analysis
Your monolith's dependency graph contains signals about natural boundaries.
Modules that frequently import each other probably belong together. Modules that rarely interact are candidates for separation.
The hypothesis is straightforward: services aligned with dependency clusters should experience fewer cross-service calls, fewer distributed transaction scenarios, and higher deployment independence compared to boundaries drawn purely from domain models.
Let's explore what this looks like in practice.
Reading the Signals in Your Graph
Here are the key patterns to look for:
The Fan-Out Anti-Pattern: A module with 20+ outgoing dependencies signals a coordination bottleneck. These "hub modules" often indicate god objects or utility classes that need refactoring before extraction.
Clean Boundary Signal: Clusters with ≤10% cross-cluster edges are strong candidates for service extraction. High internal cohesion with minimal external coupling means lower integration cost.
The Shared Data Problem: When clusters share database tables with foreign keys, structural boundaries don't match data boundaries. Splitting requires data duplication or API indirection—verify this alignment before committing to boundaries.
Example: E-Commerce Platform Migration
Consider a typical e-commerce platform migrating from a Django monolith to microservices. This is a composite example based on common patterns we see in production systems.
The Monolith: 82,000 Lines, 340 Modules
The dependency graph shows:
- 340 modules
- 1,847 internal import relationships
Dense, but not chaotic.
The question: where to cut?
Initial Hypothesis: Business Capability Mapping
A typical architecture team might propose these service boundaries:
- User Service: Authentication, profiles, preferences
- Product Service: Catalog, inventory, search
- Order Service: Cart, checkout, order management
- Payment Service: Billing, transactions, refunds
- Fulfillment Service: Shipping, tracking, returns
Clean on paper.
But cluster analysis might tell a different story.
What the Dependency Graph Could Show
Using modularity optimization based on import patterns, you might discover clusters like:
Cluster 1 (67 modules):
Identity, authentication, user profiles, preferences, user-generated content, reviews, wishlists
Cluster 2 (89 modules):
Product catalog, inventory tracking, pricing, promotions, search indexing, recommendations
Cluster 3 (34 modules):
Shopping cart, checkout flow, order state machine, payment processing, fraud detection
Cluster 4 (52 modules):
Warehouse management, shipping calculation, carrier integration, tracking, returns processing
Cluster 5 (41 modules):
Customer support, refunds, disputes, admin tools, reporting, analytics
- Cross-cluster dependencies: 127 edges (7%)
- Within-cluster dependencies: 1,720 edges (93%)
The surprises:
- Payments clustered tightly with Orders
- Pricing and Promotions clustered with Products
- Customer Support emerged as its own cluster
- User-generated content clustered with Identity, not Products
These aren't domain-driven decisions. They're structural realities.
Why the Differences Matter
Imagine the team splits Payments into its own service. The dependency graph might reveal synchronous call patterns like:
Cart → calculate_total() → get_current_promotions()
Cart → create_order() → validate_payment_method()
Order → process_payment() → handle_payment_response()
Order → payment_succeeded() → update_inventory()
Every order could touch Payments 4–6 times synchronously.
Splitting would introduce:
- Network latency per call
- Partial failure scenarios
- Distributed transaction complexity
- Retry and idempotency logic
The graph exposes that cost before any code is rewritten.
Dependency analysis is a decision aid — not a replacement for architectural judgment — but it surfaces trade-offs early, when they're still cheap.
A Potential Migration Strategy
Phase 1: Extract the Cleanest Boundary (Fulfillment)
In our example, Cluster 4 had:
- Minimal incoming dependencies
- No circular dependencies
- Clear external interface
This represents the lowest-risk extraction.
Phase 2: Split Product Catalog (But Keep Pricing)
The temptation might be to split catalog, pricing, and inventory.
But if co-change analysis shows:
catalog.py ↔ pricing.py: changed together 67%catalog.py ↔ inventory.py: changed together 71%pricing.py ↔ promotions.py: changed together 83%
They likely represent one cohesive concept: "What can we sell and for how much?"
Splitting could create constant cross-service coordination.
Phase 3: The Surprise Service (Customer Support)
If Customer Support forms its own tightly coupled cluster, it might deserve service status even if it wasn't a domain-driven priority.
Rather than scattering support logic across services, treating it as its own bounded context focused on exceptions and manual intervention could reduce complexity.
Phase 4: The Controversial Decision (Orders + Payments)
Orders and Payments feel separate in business language.
But if structurally:
- 43% of Order changes touch Payment
- 78% of Payment changes touch Order
- Multiple synchronous interactions per request
- Shared database relationships exist
Merging them into a Transaction Service might be the pragmatic choice.
Trade-off: larger service.
Benefit: avoided distributed sagas, partial failures, and excessive latency.
Measuring Success: What to Track After Migration
You've made the split. How do you know if you chose the right boundaries?
Deployment Independence Rate
What to measure: Percentage of deployments that affect only one service.
Healthy target: 80%+ of deployments are single-service changes
Warning sign: < 50% means your boundaries are still coupled
Action: If you're consistently deploying 3+ services together, those services share a bounded context and should likely be merged.
Cross-Service Call Ratio
What to measure: Number of synchronous service-to-service calls per user request.
Healthy: < 3 calls per request on average
Concerning: 5-7 calls (network latency compounds)
Red flag: > 10 calls (you've built a distributed monolith)
Why this matters: Each service hop adds latency (typically 10-50ms) and introduces a failure point. A checkout flow requiring 12 service calls has a compounded failure rate that makes SLOs nearly impossible to meet.
Distributed Transaction Frequency
What counts as a distributed transaction:
- Any workflow requiring 2+ services to succeed atomically
- Saga patterns with compensating transactions
- Two-phase commit protocols
- Manual reconciliation processes
Healthy: < 5% of operations require distributed transactions
Warning sign: > 20% of operations cross service boundaries transactionally
Reality check: If every order requires coordinating 4 services, you split payment/inventory/fulfillment incorrectly.
The cost: Distributed transactions are 10-100x more complex than local transactions. Each one requires:
- Idempotency keys
- Retry logic
- Timeout handling
- Compensating transaction design
- Manual reconciliation procedures
Data Duplication Impact
When duplication is healthy:
- Read-only reference data (product categories, country codes)
- Denormalized views for query optimization
- Event-sourced projections
- Cached frequently-accessed data
When duplication signals wrong boundaries:
- Writable data synchronized across services
- Foreign key relationships spanning services
- Frequent data consistency issues
- Manual reconciliation jobs
What to measure: Percentage of data that must be kept in sync across services. If > 30% of your database is duplicated and synchronized, your service boundaries don't match your data model.
Change Coupling After Split
Re-run co-change analysis quarterly:
# Analyze commit patterns across service repositories
for repo in service-*; do
cd $repo
git log --pretty=format:'%H %ad' --date=short --since="3 months ago"
done | sort -k2
What you're looking for:
- Services that always get deployed together (merge them)
- Changes that ripple across 3+ services (wrong abstraction boundary)
- Services that never change (consider merging into a platform service)
Healthy pattern: 90% of feature work touches 1-2 services
Warning pattern: Most features require changes to 4+ services
When the Monolith is the Right Answer
Cluster analysis might reveal clean boundaries—but that doesn't mean you should act on them immediately.
You're Still Finding Product-Market Fit
If your domain model changes every quarter, service boundaries will be obsolete before you finish the migration.
Signs you're not ready:
- Pivoting features every 3-6 months
- Changing core business logic frequently
- Adding/removing major product areas
- Rebuilding data models repeatedly
Stay monolith until: You have 12+ months of stable core domain concepts. Let the architecture emerge from validated business needs.
Your Team is < 10 Engineers
The operational complexity of microservices costs more than the coordination overhead you're solving.
Infrastructure microservices require:
- Service mesh or API gateway
- Distributed tracing (Jaeger, Zipkin)
- Centralized logging (ELK, Datadog)
- Service discovery
- Circuit breakers and retry logic
- Multiple deployment pipelines
- Cross-service monitoring
This requires dedicated platform/DevOps resources. If your entire engineering team fits comfortably into a single planning meeting, you probably can’t afford the operational overhead.
Consider services when: You have dedicated SRE or platform ownership, or your engineering organization has grown beyond informal coordination and infrastructure investment starts reducing friction instead of adding it.
Your Traffic Doesn't Justify It
If you're handling < 100 requests/second, a well-structured monolith will outperform a poorly-implemented microservices architecture.
Vertical scaling is still viable when:
- You can handle peak load on a single large instance
- Your database isn't the bottleneck
- Response times meet SLOs with room to spare
The math: A 16-core server with 64GB RAM can comfortably handle thousands of requests per second with a well-optimized monolith. That's enough headroom for most early-stage products.
Horizontal scaling matters when:
- Individual instances max out CPU/memory
- You need geographic distribution
- You have genuinely independent scaling needs (e.g., video processing vs. API requests)
The Technical Debt Isn't Architectural
Sometimes the problem isn't the monolith—it's the code quality.
Bad reasons to split:
- "The codebase is messy" → Microservices don't fix messy code, they distribute it
- "We have circular dependencies" → Fix them in the monolith first
- "Deployments are slow" → Optimize your CI/CD pipeline
- "We can't understand the codebase" → Improve documentation and onboarding
Good reason to split:
- You've already refactored, eliminated god objects, broken circular dependencies, and established clear module boundaries—and coordination costs still dominate.
The Pragmatic Decision
The best predictor of microservices success: Can you cleanly split your data model?
If your database has:
- Dense foreign key relationships across would-be services
- Shared tables that multiple services write to
- Complex joins spanning service boundaries
Then you're not ready. Data boundaries drive service boundaries. Get the data model right in the monolith, then consider extraction.
Applying This to Your Monolith
Step 1: Generate Dependency Data
Review:
- Cluster breakdown
- Cross-cluster dependency density
- Module coupling patterns
Step 2: Look for Structural Signals
- Dense internal clusters
- Thin connection points
- Hub modules
- Bridge modules
Step 3: Validate with Git History
Modules that frequently change together likely belong together.
Splitting them forces coordinated deployments — exactly what microservices are supposed to avoid.
Co-change analysis reveals conceptual coupling that static imports miss. Two modules might have zero dependency relationships but always change together—they represent the same domain concept split across files.
Here's how to surface co-change patterns:
# Find files that appear in commits together
git log --pretty=format: --name-only --since="6 months ago" \
| grep -v '^$' \
| sort \
| uniq -c \
| sort -rn
This shows change frequency. To find files that change together, use:
# Group files by commit and find pairs
git log --pretty=format:'%H' --name-only --since="6 months ago" \
| awk '/^$/ {next} /^[0-9a-f]{40}$/ {commit=$1; next} {print commit, $0}' \
| sort \
| uniq
What the percentages mean:
- > 80% co-change: These modules are essentially one concept. Splitting them creates artificial boundaries that force coordinated releases.
- 50-80% co-change: Significant conceptual coupling. Evaluate whether the coupling is domain-driven (keep together) or accidental (refactor before splitting).
- 20-50% co-change: Moderate coupling. May indicate shared utilities or cross-cutting concerns.
- < 20% co-change: Low coupling. Safe to separate if structural analysis agrees.
Why this matters: A cluster might look cohesive in the dependency graph but show low co-change rates—suggesting it's stable infrastructure that rarely evolves. That's a poor candidate for early service extraction. Focus first on high-change, high-cohesion clusters.
Step 4: Map to Domain Concepts
Now apply domain thinking:
- If clusters align with bounded contexts — great.
- If they don't — that's architectural reality.
- If a cluster crosses domain language — investigate why.
Architecture should reconcile structure and domain, not ignore one.
Step 5: Evaluate Potential Cuts
Rather than using rigid formulas, evaluate boundaries heuristically:
- Favor high internal cohesion
- Penalize synchronous cross-boundary calls
- Heavily penalize distributed transactions
- Reward genuine deployment independence
Cohesion should dominate the equation.
Common Pitfalls
Ignoring Shared Data
If two clusters share database tables with foreign keys, splitting them requires:
- Data duplication
- API indirection
- Or anti-pattern cross-database joins
Verify data boundaries match code boundaries.
Splitting Too Early
Just because you can identify boundaries doesn't mean you should create services immediately.
Good reasons to split:
- Different scaling needs
- Different deployment schedules
- Clear ownership boundaries
- Regulatory separation
Bad reasons:
- "It feels big."
- "Microservices are best practice."
Ignoring Change Frequency
A cohesive but rarely-changing cluster may not justify the operational cost of becoming its own service.
Service boundaries are about trade-offs, not purity.
The Pragmatic Path
You shouldn't extract everything at once.
- Analyze and plan
- Extract one clean boundary
- Stabilize and measure
- Iterate
Service boundaries are evolvable.
Let Your Code Guide You
Dependency graph analysis doesn't replace domain expertise. It grounds it.
Your monolith evolved through real feature development under real constraints. The dependency patterns reflect that history.
Before your next architecture whiteboard session, run the analysis. Let the code show you what it already knows.
Generate your service boundary recommendations: Analyze your monolith at pvizgenerator.com to see cluster analysis and identify natural service boundaries before you start cutting.
Next in this series: Technical Debt Archaeology — using dependency age and change patterns to prioritize what to refactor first.
Try PViz on your own codebase
Get dependency graphs, coupling signals, and a compressed bundle ready for your LLM — for any GitHub repository, in minutes.