Replacing a mature Java application is rarely just a framework upgrade. Its database, scheduled jobs, integrations, and undocumented rules may support years of business activity. Splitting the code into services can move that complexity onto the network without making the business easier to operate.
This guide helps engineering leaders plan incremental modernization while preserving a working product. It focuses on migration decisions and evidence required before each cutover. The examples are illustrative; they do not describe an ARINITS client engagement.
Choose the business outcome before the target architecture
Start with a specific constraint: releases require coordinated downtime, one workload consumes too much capacity, an unsupported runtime creates maintenance risk, or changes in one domain repeatedly break another. Establish a baseline for release frequency, recovery time, production defects, latency, and operating cost. Define how the proposed change will improve the constraint.
A modular monolith may solve ownership and coupling problems with fewer operational demands. Microservices become useful when a capability needs independent deployment, scaling, security boundaries, or team ownership. A containerized monolith is still a monolith, but moving it onto a supported runtime and repeatable deployment process can be a valuable first step.
Map dependencies and business invariants
Inventory entry points, scheduled tasks, reports, external consumers, database tables, stored procedures, batch exports, and operational scripts. Trace representative workflows through these components. Ask support and finance teams which manual workarounds are essential; repository analysis alone will not reveal every dependency.
Document the rules that must remain true throughout migration. For example, an order must not be fulfilled twice, a payment must not produce duplicate financial effects, and an approval must be attributable to an authorized person. These invariants determine the transaction boundaries, tests, and reconciliation controls.
Build a dependency map around capabilities rather than packages. A package named “utilities” may conceal pricing rules shared by several workflows. A proposed service that reads ten domains' tables is unlikely to be independent merely because it exposes an HTTP endpoint.
Select a first slice with measurable value
Choose a capability with clear inputs and outputs, manageable dependencies, an accountable owner, and enough value to justify the migration. A read-oriented catalogue or notification workflow can be easier to separate than the transaction engine, but its suitability depends on the actual system. Avoid selecting a component solely because its code is small.
For an illustrative order platform, shipment tracking might be a good first slice if it consumes stable order events and does not own payment decisions. First identify every legacy reader and writer of tracking state. Then define a contract for status changes, duplicate events, late updates, and unavailable downstream providers.
Write acceptance criteria before extraction: equivalent business behavior, documented consistency, agreed latency, observable failures, and a tested cutover procedure. Also define when to stop. If ownership remains unclear or the new boundary requires chatty calls during every transaction, refine the module before distributing it.
Introduce a routing seam with the strangler fig pattern
The strangler fig approach replaces capabilities gradually while the existing application continues serving the remaining workload. A routing layer can direct selected operations to the new implementation. AWS's pattern guidance highlights domain-boundary uncertainty and the risk of making that routing layer a bottleneck. Martin Fowler's original discussion explains the incremental replacement idea.
Make routing rules explicit and reviewable. A route may depend on capability, tenant cohort, or a controlled feature flag. Preserve authentication, authorization, request limits, and timeouts across both paths. Use an adapter to translate legacy contracts into the new domain model, and give temporary adapters a removal owner and exit condition.
Shadow execution is useful for comparing read results. It is dangerous for commands that send messages, create orders, or charge customers. Compare such commands using isolated replay fixtures or a side-effect-free evaluation mode. Never execute a live financial command twice just to compare implementations.
Separate data ownership before claiming independence
Identify the authoritative writer
For each migrated entity, name one authoritative owner. If both applications can update the same record without a coordination contract, conflicts and ambiguous rollback become likely. A shared database may be a temporary migration seam, but it should have restricted access, a defined duration, and a plan to remove direct table dependencies.
Plan the transition from historical to live data
Backfill historical records with stable identifiers and checkpoints. Verify row completeness, relevant totals, reference integrity, and representative business states. Capture changes that occur during the backfill, then establish a cutover position so there is no gap between historical import and live processing. Track duplicates and schema versions explicitly.
Make consistency a product decision
If a downstream read model receives events asynchronously, document how stale it may become and what users see while it catches up. Financial state, inventory reservations, and access revocation can require different consistency policies. Do not apply eventual consistency to every workflow merely because messaging is available.
Avoid an unprotected database-plus-message dual write. When a state change must emit an event, consider a transactional outbox and idempotent consumers. ARINITS's payment idempotency guide explains duplicate and recovery concerns; those controls remain relevant during migration.
Define rollback around data, not only traffic
Returning requests to the old endpoint is safe only if the old application can understand the state produced by the new one. Once ownership transfers, rollback may need reverse synchronization, compatibility handling, or a forward repair. Specify the maximum supported rollback window and what makes it close.
Use additive schema changes first. Introduce a new field, allow compatible readers and writers, migrate data, verify usage, and remove obsolete fields only after the old code is retired. Keep database compatibility and application deployment as separate decisions.
A cutover runbook should identify the decision owner, prerequisites, checkpoints, pause conditions, recovery commands, and reconciliation checks. For the tracking example, compare processed event positions, unresolved deliveries, and status history before shifting traffic. Agree how new writes will be handled if the service must be disabled.
Prove readiness with failure-focused validation
Characterization tests capture legacy behavior where specifications are incomplete. Contract tests protect interfaces between old and new components. Representative replay data helps compare results, but it must be sanitized and must not trigger live side effects. Include edge cases, rather than only successful examples.
Exercise timeouts, duplicate events, out-of-order updates, worker crashes, dependency failures, and authorization changes. Verify that recovery preserves the business invariants. A green unit-test suite does not prove that a partially completed workflow is recoverable after a cutover.
Instrument the migration before sending production traffic. Compare outcome rates, latency, error codes, backlog, and data differences by cohort. The Spring Boot observability guide describes how logs, metrics, and traces can support this diagnosis. Observability should tell the operator when to pause, not simply provide more dashboards.
Retire the old path deliberately
After the new capability stabilizes, remove obsolete routes, writers, jobs, integrations, and permissions. Confirm that reporting and recovery procedures no longer depend on legacy tables. Archive required records under the organization's retention policy, and document the remaining system boundary.
A migration is not complete while both systems remain mandatory forever. Review each temporary bridge regularly and connect its removal to a concrete milestone. Measure the resulting delivery and operating behavior against the baseline; use that evidence to decide whether another extraction is worthwhile.
ARINITS can help assess boundaries and delivery risks through its application modernization services and Java microservices engineering. Review the enterprise microservices portfolio example, or contact ARINITS with the workflow, constraints, and migration risks you need to evaluate.
Frequently asked questions
Must a Java monolith become microservices?
No. Supported runtimes, modular boundaries, automated testing, and repeatable releases may address the business problem. Extract services when independent operation offers enough value to justify distributed-system complexity.
Can the new service share the legacy database?
Temporarily, if ownership and access are controlled. Treat direct table access as a migration dependency with an exit plan. Sharing a database indefinitely usually limits independent changes and complicates recovery.
Which capability should we migrate first?
Choose one with a clear owner, stable contract, manageable dependencies, measurable benefit, and feasible recovery. The best candidate depends on the dependency map and business risk, not a universal list.
Is routing traffic back enough to roll back?
Only when legacy code can interpret all state written during the new path. Otherwise, recovery needs data synchronization, compatibility logic, or forward repair. Test that procedure before cutover.
How should success be measured?
Compare the original constraint with the new behavior: release independence, defect rate, recovery time, latency, cost, or scaling limits. Also confirm that the old path can be retired without losing required business behavior.