Migration Guide
JSP and Java to React, without freezing the site.
A server-rendered Java frontend — JSP, JSF, or Struts, usually fronting a legacy CMS — moves to React or Next.js one route at a time. This is how the move actually goes: the routing, the session handling, where the business logic stays, and the rollback that has to work the first time it's needed.
This is a technical playbook for a specific migration. For the commercial shape of the engagement, see Legacy System Modernization. For replatforming onto a new commerce platform instead, see Platform Migration.
Where it actually breaks
Four failure modes, every time.
None of these show up in a code review. They show up two weeks after launch, in a support ticket, or in a Search Console dashboard nobody checked that week.
Session breaks mid-visit
A user logs in on the legacy JSP checkout, then hits a migrated React page that doesn't recognize the session — they're logged out on page four, not page one, which is why it never shows up in a demo.
Business logic gets copied, not called
Pricing rules, tax exceptions, and fulfillment edge cases get re-implemented inside React components because it's faster than exposing an API. Two copies of the same rule start drifting the week they ship.
SEO erodes quietly
Canonical tags point at the wrong version, sitemaps lag the actual route split, and organic traffic falls for weeks before anyone traces it back to the migration.
The cutover is a date, not a proof
Routes flip on a calendar instead of after a parallel run proves parity. The first real traffic on the new route is also the first real test of it.
Two release trains, no shared calendar
The legacy app deploys on a quarterly change-control cycle; the new frontend ships daily. Without a shared view of what's live where, a hotfix on one side can silently break a route the other side just moved.
How routes move
Strangler-fig routing, decided at the edge.
One router, one decision per request: does this route belong to the legacy templates or the new frontend today. Coverage shifts as each slice proves itself — never all at once.
Why the edge, not the origin
A router at the CDN or reverse-proxy layer can send a request to either stack without either stack knowing the other exists. That decoupling is what lets the legacy app keep deploying on its own schedule while React ships independently.
This is the same pattern behind every modernization Destm runs — named and detailed in full at the solutions page.
Session and auth continuity
Two stacks, one session.
One session store, not two
The servlet container's HttpSession can't be read by a Node process. Move session state into something both stacks can reach — Redis, a shared JWT, or a session-bridge service — before any route moves.
Token translation at the boundary
React talks to the API in bearer tokens; JSP talks in container sessions. A translation layer issues both from one source of truth so a user never re-authenticates crossing the seam.
Cookie domain scoped for both stacks
Auth cookies need a domain and path that both the legacy app server and the new frontend can read — usually the apex domain, not a subdomain scoped to one stack.
SSO handoff tested under real load
If an identity provider sits in front of both, the handoff redirect has to survive both stacks issuing it. Test this before the first checkout route moves, not after.
The demo always works. The failure shows up on page eleven, after checkout, when the token issued by JSP never reaches the layer serving the confirmation screen.
The line React must not cross
Business logic stays behind the API, always.
The pricing rules, tax exceptions, and fulfillment edge cases inside a decade-old JSP application were tuned by people who are sometimes still on the team and sometimes not. Either way, they took years to get right.
Re-implementing them inside React components is faster in the first sprint and wrong by the second quarter. The moment a rule lives in two places, one of them will drift — a tax exception patched in the Java service and forgotten in the frontend, or the reverse.
The fix is an anti-corruption layer: a typed API the new frontend calls, backed by the same Java service that has always owned the rule. React renders what the API returns. It never recalculates a price, a discount, or a delivery estimate on its own.
This is also the fastest way to migrate, not just the safest. A frontend that only renders is a frontend that can move route by route without waiting for a parallel rewrite of rules nobody fully wrote down the first time.
Copying business rules into React isn't a migration. It's forking a decade of pricing and fulfillment logic into two places that will drift apart within a quarter.
SEO parity during the move
Rankings survive the move or they don't.
Organic traffic is the metric that punishes a phased migration for mistakes made in week one and not caught until week six.
Canonical tags follow the route, not the stack
Whichever stack currently serves a route owns its canonical tag. When the route moves, the canonical moves with it — never two canonicals pointing at each other.
Sitemap regenerated per phase
A static sitemap written for the pre-migration site goes stale the day the first slice ships. Regenerate it from the router's actual route table, not from a spreadsheet.
Redirect chains resolved, not stacked
If a URL structure changes as part of the move, redirect straight to the final destination. A chain of three 301s is a chain of three chances to lose link equity.
Crawl behavior monitored, not assumed
Watch Search Console crawl stats and indexed-page counts weekly during the migration. A drop in crawled pages is the earliest signal something in the router is misrouting the crawler.
Google's own guidance on site moves with URL changes treats a phased migration the same way we do — as a sequence of small, verifiable moves, not one event.
Side by side
What actually changes under the hood.
| Dimension | JSP / Struts | React / Next.js |
|---|---|---|
| Rendering model | Server-rendered per request — JSP compiles to a servlet, runs, and returns HTML. | React Server Components render on the edge or origin; the client hydrates for interactivity. |
| Session state | Held in the servlet container's HttpSession, tied to that JVM instance. | Stateless — auth travels as a token, session detail lives in a shared store both stacks can read. |
| Business logic location | Mixed into the view — scriptlets, custom tags, and controller code sit next to markup. | Isolated behind an API boundary. The frontend renders what the API returns and nothing more. |
| SEO control | Bound to app-server response timing and whatever caching the ops team configured. | Per-route control — static generation, incremental regeneration, or streaming, chosen per page type. |
| Deployment cadence | Full WAR or EAR redeploy, often gated by a change-control process measured in weeks. | Route-level deploys, often the same day, with instant rollback to the previous build. |
| Performance ceiling | Bound by servlet thread pools and template compilation on every uncached request. | Edge-cacheable, code-split by route, streamed — closer to the actual Core Web Vitals ceiling. |
Why this earns its cost
Performance is the number that pays the bill.
A JSP page compiles server-side on every uncached request and ships whatever markup and script weight accumulated over a decade of feature additions. That combination puts Core Web Vitals out of reach no matter how well the JVM side is tuned — the render path itself is the ceiling.
On a national gifting brand's storefront, migrated catalog and checkout paths moved Lighthouse scores into the high 90s — the direct result of edge caching, code-splitting by route, and dropping the template-compile step on every request.
That number is the business case. A migration that doesn't move Core Web Vitals meaningfully is a rewrite with better developer experience, not a project that pays for itself in conversion.
The rollback
A rollback is a running system, not a document.
A rollback plan written in a wiki page and never executed is a hypothesis. The edge router that sends a route to React can send it back to JSP with the same one-line change — that reversibility only exists if the legacy path is still live, still deployed, and still exercised.
Keep the legacy route serving a small slice of real traffic — a canary — even after the new route looks stable. The day the router flips a route back under load is not the day to discover the legacy service has drifted out of a deployable state.
Parity monitoring runs both ways: the new route is checked against the old one before cutover, and the old one stays checked against the new one until the legacy code path is finally retired.
A rollback that has never been executed is a document, not a rollback.
FAQ
Questions engineering leads ask before signing off.
QHow long does a JSP-to-React migration take?
How long does a JSP-to-React migration take?
It runs route by route rather than against a fixed calendar. Coverage grows as each slice proves parity in a parallel run, so the honest answer depends on how many distinct route families the storefront has — checkout, catalog, account, and content sections each move as their own slice.
QDo we have to freeze feature work on the legacy JSP app during the migration?
Do we have to freeze feature work on the legacy JSP app during the migration?
No. Strangler-fig routing at the edge means the legacy app keeps shipping changes on its own routes right up until they move. Freezing the legacy app for the length of a migration is exactly the risk this pattern is built to avoid.
QWhat happens to the CMS fronting the JSP templates?
What happens to the CMS fronting the JSP templates?
It keeps serving content for any route that hasn't migrated yet. Once a content-driven section moves, its content model gets its own clean boundary — usually an API the new frontend calls, not a rewrite of the CMS itself.
QCan organic rankings survive a phased move?
Can organic rankings survive a phased move?
Yes, if canonical tags, sitemap generation, and redirect chains are treated as part of the migration plan from week one — not patched in after traffic drops. Crawl monitoring during the move catches problems while they're still cheap to fix.
QShould the new React frontend call the legacy Java backend directly?
Should the new React frontend call the legacy Java backend directly?
Through a typed API boundary, not directly against internal servlets or the database. That boundary is also where business logic stays — the frontend renders, it doesn't recalculate tax or reprice a cart.
QWhat proves a migrated route is actually done?
What proves a migrated route is actually done?
A parallel run where the new route's output matches the legacy route's output against real traffic — not a visual review that says it looks about right. Parity proven in production is the bar, because that's the bar the rollback plan is built against.
How an engagement starts
Three steps to a partnership
Intake call
30 minutes. We listen, you talk. No deck.
Diagnostic
We audit the surface, name the bottleneck, propose a path.
Kickoff
Senior engineer in your standup by week two.
Move it route by route, not all at once
Tell us what the JSP app runs today. We'll come back with the route order, the session-continuity plan, and where the first production slice should land.