From Play Framework to Spring Boot without a big rewrite
TL;DR - The core of our platform was on Play Framework, and every change got slower. I didn’t stop the team for a rewrite. Spring Boot runs next to Play, new and changing flows move over one at a time, and each move starts with tests that describe what the old code does today, bugs included.
When I took over the platform, the backend was a Play Framework application. It worked and customers depended on it. The problem was everything around it: a shrinking ecosystem, fewer people who know it, and upgrades that got harder every year. New hires knew Spring. Nobody knew Play.
The easy pitch was “let’s rewrite it in Spring”. I’ve seen that pitch before: a year of rewrite while the old system keeps getting new features, and two systems to maintain at the end.
So I went with the strangler approach: Spring Boot grows around Play until Play has nothing left to do.
flowchart LR
Client[Web, mobile, integrations] --> Router[Routing layer]
Router -->|migrated routes| Spring[Spring Boot]
Router -->|everything else| Play[Play Framework]
Spring --> DB[(PostgreSQL)]
Play --> DB
What moves first
Not the ugliest code. Two questions decide it:
- How often does this flow change? That’s where Play slows us down every sprint.
- How easy is it to isolate? Few tables, clear inputs and outputs.
A flow that never changes can stay on Play for a long time. It costs nothing while it sits there.
Tests before moving anything
Before a flow moves, I write characterization tests: tests that describe what the Play code does today, not what it should do. Then the Spring version has to pass the same tests. When one fails, I decide on purpose whether it’s a bug to fix or behavior a customer depends on.
AI helped most here. I’d give it a Play controller with its nested conditions and ask for every path through it and a test for each. It found branches I’d missed. I still read every test, because sometimes it described behavior the code didn’t have.
One database, one owner per table
Both apps use the same PostgreSQL database during the migration. Not elegant, but it means no data sync and no “which side is right” arguments. The rule: a table has one owner. When a flow moves to Spring, its tables move with it, and Play can only read them.
What I learned
The migration isn’t a project with an end date. It’s a habit: when a flow needs a big change, it moves instead of getting patched. The team keeps shipping, and Play gets smaller every month.