Strangler Fig Pattern: How to Migrate a Java Monolith to .NET Incrementally
You have a Java monolith. Management wants .NET. Rewriting everything at once is a fantasy — it’s a multi-year, high-risk project that rarely succeeds. The strangler fig pattern offers a proven alternative: wrap the old system, incrementally replace components, and eventually decommission the monolith — all while the system stays in production.
This guide walks through applying the strangler fig pattern specifically to Java-to-.NET migrations, with practical strategies for routing, data synchronization, bridge integration, and the organizational challenges that determine whether your migration succeeds or stalls halfway.
What Is the Strangler Fig Pattern?
Named after strangler fig trees that grow around a host tree and eventually replace it, the pattern works in three phases:
- Wrap: Place a routing layer in front of the existing Java monolith. All traffic flows through this layer.
- Replace: Build new functionality in .NET. Route specific requests to the new .NET services while the rest continues to hit the Java monolith.
- Remove: As .NET services prove stable, decommission the corresponding Java components. Eventually, the monolith is empty and can be shut down.
Phase 1: Wrap Phase 2: Replace Phase 3: Remove
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ API Gateway │ │ API Gateway │ │ API Gateway │
│ (routing layer) │ │ (routing layer) │ │ (routing layer) │
└────────┬─────────┘ └───┬──────────┬───┘ └────────┬─────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────────┐ ┌─────────────┐ ┌─────────┐ ┌──────────────────┐
│ Java Monolith │ │Java Monolith│ │.NET Svc │ │ .NET Services │
│ (100% traffic) │ │(shrinking) │ │(growing) │ │ (100% traffic) │
└──────────────────┘ └─────────────┘ └─────────┘ └──────────────────┘Why Strangler Fig for Java-to-.NET Specifically
The strangler fig pattern is particularly well-suited to Java-to-.NET migrations because:
- Both platforms are enterprise-grade. Unlike migrating from a legacy language to a modern one, Java and .NET are both actively maintained with large ecosystems. The migration is about strategic alignment, not escaping a dead platform.
- The business logic is the hard part. Java and .NET share similar paradigms (OOP, garbage collection, strong typing). The challenge isn’t translating syntax — it’s migrating years of accumulated business rules without breaking them.
- Bridge tools enable coexistence. Tools like JNBridgePro allow Java and .NET components to call each other directly during the migration, avoiding the need to build temporary APIs for every migrated component.
- Teams can upskill gradually. Java developers can learn C# while working on new features in .NET, rather than stopping all feature work for a big-bang rewrite.
Planning a Java-to-.NET migration? JNBridgePro serves as the bridge layer during strangler fig transitions, enabling incremental migration without big-bang risk — try a free evaluation.</p
What Is the Strangler Fig Pattern?
The strangler fig pattern is an architectural migration strategy that incrementally replaces a legacy system (such as a Java monolith) with a new system (such as .NET microservices) by routing traffic through a facade layer. Instead of a risky big-bang rewrite, you “strangle” the old system one component at a time — new features go to the new system, while existing functionality migrates gradually over 12–30 months. The pattern is named after strangler fig trees, which grow around a host tree and eventually replace it entirely.
Strangler Fig Migration Steps
- Map the monolith — identify domain boundaries, dependency graphs, and migration candidates
- Build the routing layer — deploy a facade (API gateway or reverse proxy) that routes requests to old or new code
- Migrate one component — rewrite or bridge the highest-value, lowest-risk module first
- Bridge shared state — use a shared database, event sourcing, or in-process bridge (like JNBridgePro) to keep old and new code synchronized
- Route traffic incrementally — shift traffic from old to new, validate with metrics, roll back if needed
- Decommission the old component — once all traffic routes to the new system, remove the legacy code
>
Step-by-Step Migration Strategy
Step 1: Map the Monolith
Before writing any .NET code, understand what you’re dealing with:
- Domain boundaries: Identify logical modules within the Java monolith. These become candidates for independent .NET services. Look for natural seams: packages with minimal cross-references, distinct database tables, separate business workflows.
- Dependency graph: Map which Java classes depend on which. Tools like JDepend, Structure101, or a simple
jdepsanalysis reveal the coupling. Highly coupled areas migrate together; loosely coupled areas can migrate independently. - Data ownership: Determine which module “owns” which database tables. Shared tables are migration blockers that need special handling.
- Traffic patterns: Identify which features handle the most traffic and which are most critical. Migrate low-risk, low-traffic features first.
# Analyze Java dependencies
jdeps --summary --multi-release 21 myapp.jar
# Output helps identify migration boundaries:
# com.company.billing -> com.company.core (tight coupling)
# com.company.reporting -> com.company.core (loose coupling) ← migrate first
# com.company.auth -> java.base (self-contained) ← migrate firstStep 2: Install the Routing Layer
The routing layer is the foundation of the strangler fig. It sits in front of the Java monolith and will eventually route traffic to .NET services:
# NGINX routing layer example
upstream java_monolith {
server java-app:8080;
}
upstream dotnet_reporting {
server dotnet-reporting:5000;
}
server {
listen 80;
# Phase 1: Everything goes to Java
# Phase 2: Reporting migrated to .NET
location /api/reports {
proxy_pass http://dotnet_reporting;
}
# Everything else still goes to Java
location / {
proxy_pass http://java_monolith;
}
}Alternatives to NGINX: API gateways (Kong, AWS API Gateway), service meshes (Istio, Linkerd), or a custom .NET reverse proxy (YARP). Choose based on your existing infrastructure.
Step 3: Migrate the First Feature
Pick a feature that is:
- Low risk (non-critical if it fails temporarily)
- Low coupling (minimal dependencies on other Java code)
- Well-understood (team knows the business rules)
- Has good test coverage (you can verify the migration)
Common first candidates: reporting, notifications, search, user preferences, or audit logging.
// .NET implementation of the migrated reporting service
[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
private readonly IReportingService _reportingService;
private readonly IJavaLegacyBridge _legacyBridge; // For data still in Java
[HttpGet("{reportId}")]
public async Task<IActionResult> GetReport(string reportId)
{
// New .NET logic for report generation
var report = await _reportingService.GenerateReport(reportId);
// Some data might still come from the Java monolith
// Bridge call for legacy data during transition
var legacyData = _legacyBridge.GetHistoricalData(reportId);
report.AppendLegacyData(legacyData);
return Ok(report);
}
}Step 4: Handle the Hard Part — Shared State
The biggest challenge in strangler fig migrations isn’t building new services — it’s managing shared state between the old Java system and the new .NET services. Three approaches:
Approach A: Shared Database
Both Java and .NET read/write the same database. Simple but creates tight coupling.
// Both sides access the same tables
// Java (existing): SELECT * FROM orders WHERE status = 'pending'
// .NET (new): SELECT * FROM orders WHERE status = 'pending'
// Risk: Schema changes must be coordinated across both platforms
// Mitigation: Use database views as stable interfacesApproach B: Event-Driven Synchronization
Java publishes domain events to a message broker; .NET subscribes and maintains its own data store.
// Java monolith publishes events
public class OrderService {
@Autowired private KafkaTemplate<String, OrderEvent> kafka;
public void processOrder(Order order) {
// Existing business logic
order.setStatus(Status.PROCESSED);
orderRepo.save(order);
// NEW: Publish event for .NET consumers
kafka.send("order-events", new OrderProcessedEvent(order));
}
}
// .NET service consumes events
public class OrderEventConsumer : IHostedService
{
public async Task ProcessMessage(OrderProcessedEvent evt)
{
// Maintain .NET's own read model
await _reportingDb.UpsertOrder(evt.ToReportingModel());
}
}Approach C: Bridge-Based Data Access
Use an in-process bridge to call Java data access methods directly from .NET, avoiding data duplication entirely:
// .NET service calls Java DAO via JNBridgePro bridge
public class BridgedOrderRepository : IOrderRepository
{
private readonly com.company.dao.OrderDAO _javaDao;
public BridgedOrderRepository()
{
// Bridge loads the Java DAO in-process
_javaDao = new com.company.dao.OrderDAO();
}
public Order GetOrder(string orderId)
{
// Call Java method directly — no REST, no Kafka, no data sync
var javaOrder = _javaDao.findById(orderId);
return OrderMapper.ToDotNet(javaOrder);
}
}
// When ready to fully migrate, swap the implementation:
public class NativeOrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public Order GetOrder(string orderId)
{
return _db.Orders.Find(orderId);
}
}The bridge approach is uniquely powerful for strangler fig migrations. It lets .NET services access Java data and logic without duplicating databases or building event pipelines. When a component is fully migrated, you simply swap the bridged implementation for a native one.
Step 5: Expand and Repeat
With the first feature proven, expand the migration systematically:
- Migrate by domain boundary, not by technical layer. Don’t migrate “all controllers” or “all repositories” — migrate “all of billing” or “all of reporting.”
- Update the routing layer for each migrated feature. The API gateway configuration becomes your migration progress tracker.
- Run parallel verification: For critical features, run both Java and .NET implementations simultaneously, compare outputs, and alert on discrepancies.
- Track metrics per feature: Response time, error rate, and resource usage for each migrated feature. Ensure .NET performs at least as well as Java.
Step 6: Decommission
When a Java component has zero traffic for 30+ days:
- Remove the routing rule (fail-safe: 404 instead of routing to dead code)
- Archive the Java source code (don’t delete — you may need it for reference)
- Drop the Java database tables (after confirming .NET has its own data store)
- Remove the Java JARs from the deployment
- Update documentation and runbooks
Real-World Timeline
Based on typical enterprise migrations:
| Phase | Duration | Java Traffic | .NET Traffic | Key Activity |
|---|---|---|---|---|
| Setup | 1-2 months | 100% | 0% | Install routing layer, map monolith, pick first feature |
| Pilot | 2-3 months | 95% | 5% | First feature migrated, team learning .NET |
| Expansion | 6-12 months | 50% | 50% | Multiple features migrated, bridge handles cross-cutting calls |
| Majority | 6-12 months | 10% | 90% | Core features on .NET, Java handling legacy edges |
| Cleanup | 2-3 months | 0% | 100% | Decommission Java, remove bridge, archive code |
Total: 18-30 months for a medium-complexity monolith (~100K-500K lines of Java). Larger systems take longer. The key advantage over a big-bang rewrite: the system is in production throughout, and value is delivered incrementally.
Common Pitfalls
- Migrating bottom-up (infrastructure first). Don’t start by migrating the database layer or shared libraries. Start with user-facing features that deliver visible value. Infrastructure changes follow when needed.
- Not investing in the routing layer. A brittle routing layer blocks the entire migration. Invest in proper API gateway configuration, health checks, and circuit breakers from day one.
- Attempting to migrate shared libraries early. Cross-cutting concerns (logging, auth, config) are the hardest to migrate because everything depends on them. Use the bridge to keep calling Java shared libraries from .NET until late in the migration.
- Losing momentum. Strangler fig migrations stall when teams get pulled to other priorities. Set quarterly milestones and track migration percentage as a KPI.
- Underestimating data migration. Moving business logic is straightforward. Moving data with zero downtime — especially with referential integrity and running transactions — is the hard part. Plan for it early.
- Skipping parallel verification. “It works in staging” isn’t enough for critical features. Run both implementations in production and compare results before cutting over.
How JNBridgePro Accelerates Strangler Fig Migrations
JNBridgePro addresses the two hardest problems in strangler fig migrations:
- Temporary integration without temporary APIs: During migration, .NET services need to call Java components that haven’t been migrated yet. Instead of building REST APIs for every temporary touchpoint, JNBridgePro lets .NET call Java methods directly. When the Java component is eventually migrated, swap the bridge call for a native .NET call.
- Incremental data access: Instead of duplicating databases or building event pipelines, .NET services can use JNBridgePro to call Java data access objects directly. This eliminates the most complex part of the migration until you’re ready to move the data store.
- Risk reduction: If a .NET migration of a specific feature doesn’t work out, rolling back is trivial — just update the routing rule. The Java code is still there, still working, still callable via the bridge.
Conclusion
The strangler fig pattern transforms a high-risk, multi-year Java-to-.NET rewrite into a series of manageable, low-risk incremental migrations. Each migrated feature delivers value immediately, teams learn .NET progressively, and the system never goes offline.
The key enabler is a bridge that lets Java and .NET coexist during the transition. JNBridgePro provides that bridge — letting .NET services call Java methods directly while you migrate at your own pace, without building throwaway APIs or duplicating data infrastructure.
