Founder field note · 2019
Migrating a Lending Workflow from a Monolith to a Microservice
A European lending platform had been trying to move one loan-review workflow out of its monolith for roughly two years.
Two teams had already tried. Both attempts failed. Then a three-person team - one tech lead and two developers - finished the migration in two months.
This is a personal field note from work I led as a tech lead in 2019. The client is anonymized, but the engineering pattern is common: a monolith with too many teams inside it, a migration that keeps slipping, and a business that needs the work finished without breaking production.
Key results
- Turned a migration that had been stalled for roughly two years into a two-month delivery.
- Shipped with a three-person team.
- Kept the existing API contract, so the UI team did not need to rewrite its side.
- Used real production traffic in shadow mode to find missing business logic before cutover.
- Moved production traffic from the monolith to the new microservice.
- Released without needing to roll back to the monolith or handle post-cutover production incidents.
- Kept response latency in the same range after cutover.
- Moved ownership to one team with its own release path.
- Improved delivery speed by multiples because future changes no longer had to move through the shared monolith release process.
Case snapshot
- Industry: European lending / fintech
- System: loan application workflow inside a shared monolith
- Team: 1 tech lead and 2 developers
- Timeline: 2 months
- Constraint: no frontend rewrite, no unsafe cutover, no visible production disruption
- Pattern: API-compatible replacement, shadow traffic, incremental cutover
The client context
The platform processed loan applications.
Loan review was split into seven approval queues, based on the size and importance of the application. A small application could go to a lower queue. A very large application could go to the highest queue for review, approval, or rejection.
The workflow we migrated selected the most relevant loan application for each approval queue to process next.
It sounds narrow. It was not.
The workflow sat inside a large monolith shared by many engineering teams. Around ten teams were adding code to the same system. Every release made the monolith harder to reason about. Performance got worse. Bugs became harder to trace. Ownership was unclear because too many teams touched the same code and the same flows.
For the business, this showed up as slower feature delivery. The company needed teams to own their domains, scale them independently, and move faster without waiting for the whole monolith to change safely.
Why the first attempts failed
The first team followed the classic migration path.
They refactored the monolith, tried to separate the business logic, moved related code into a new service, and started working on the data migration.
The result was worse performance. After the refactoring, performance dropped by more than 10x, according to the project team's internal assessment. That attempt was stopped.
The second team went further.
They moved business logic, prepared a database migration, and wrote scripts to move data from the monolith to the new service.
But they could not release safely. Every time traffic moved to the new service, users found bugs. The team had to switch back to the monolith.
They also asked the UI team to rewrite the frontend for the new API. That made the release depend on another team and turned the migration into a larger, riskier change.
The diagnosis
The problem was not only the monolith. It was the migration strategy.
The previous teams tried to clean up the domain first. They tried to create better abstractions inside a monolith that was still changing every day.
That was the wrong order.
With many teams still adding features to the monolith, every new abstraction became a moving target. By the time one part was cleaned up, another part had changed. The migration stayed open too long, and the risk kept growing.
The better path was to make the new service behave exactly like the old API first.
That meant:
- keep the same endpoints;
- accept the same request parameters;
- return the same response structure;
- avoid asking the UI team to change anything;
- prove that the new service behaved like the monolith before sending users to it.
The migration had to be short, practical, and focused on behavior.
The approach
We built the microservice as a compatible replacement for the old monolith API.
Instead of starting with a large UI change or a full domain rewrite, we worked at the API level. The frontend could keep calling the same endpoints. The new service had to match the old behavior.
Then we added shadow testing.
The real user request still went to the monolith. That response was returned to the user, as before. In the background, the same request was copied and sent asynchronously to the new microservice.
We used queues to process those copied requests without slowing down production traffic.
Then we compared the monolith response with the microservice response. When the responses were different, we investigated why. Usually, it meant some hidden business rule, table, dependency, or edge case had not been moved yet.
This changed the migration from guesswork into a feedback loop:
- Copy real traffic.
- Run it against the new service.
- Compare the result.
- Find the missing business logic.
- Fix the service.
- Repeat.
How we reduced risk
We did not try to migrate everything at once.
We started with read operations for the first approval queue. Then we moved through the next queues one by one.
Once read operations matched the monolith, we moved to write operations. The same idea applied: compare behavior, find gaps, fix the service, and keep the database synchronized.
We also kept normal service-level tests.
Unit and integration tests were moved with the service. Integration tests used Testcontainers. Shadow testing did not replace tests. It added a second layer of confidence by checking the service against real production behavior.
This was different from the previous approach. Earlier teams had tried to rely heavily on slow UI end-to-end tests. Those tests were expensive to build and slow to run. API-level comparison gave faster feedback and covered the business scenarios users were actually triggering.
The cutover
After the service matched the monolith for read and write operations, production traffic was moved to the microservice.
The cutover did not require a rollback.
After release, future changes for this workflow were made in the microservice instead of the monolith. The team responsible for the monolith was asked to remove the old API and the related internal logic so nobody could continue using the legacy path.
Then our team moved on to the next migration.
The result
The migration was completed in two months by a team of three people.
No rollback was needed after the final cutover, and the cutover did not create a new production incident stream. From that point on, changes for this workflow moved through the microservice.
The new microservice reduced the workload that stayed inside the monolith. It also gave the workflow its own scaling path, ownership boundary, release cadence, and place for future performance improvements.
Delivery improved by multiples because the workflow was no longer competing with around ten teams inside the same monolith release path. Ownership moved to one team, and that team could release changes independently without asking the whole monolith to move with it.
Response latency stayed in the same range after the migration. The point was not to make the workflow visibly faster on day one. The point was to move it into a safer place where performance, ownership, and delivery could improve without reopening the monolith.
Most importantly, the team finished a migration that had stayed unresolved for roughly two years.
The result was not a cleaner diagram. The result was a working production service that replaced a risky part of the monolith without forcing a frontend rewrite.
Why it worked
The team made one important decision: preserve the API contract first, improve the internals later.
That decision kept the migration small enough to finish.
It removed the dependency on the UI team. It avoided a long domain refactor inside a moving monolith. It let us use real traffic to find missing behavior. And it made the final cutover a controlled switch, not a bet.
For this type of migration, compatibility was more valuable than purity.
Lessons for similar teams
If a monolith migration has already failed once, the next attempt should not start by doing the same thing with a bigger team.
Start by asking:
- Can the new service keep the old API contract?
- Can we test behavior against real traffic before users depend on it?
- Can we avoid forcing another team to rewrite their side of the system?
- Can we migrate one level, operation, or workflow at a time?
- Can we finish the migration quickly enough that the monolith does not move away from us?
A long migration against a fast-changing monolith becomes a race you usually lose.
A short, behavior-first migration gives you a better chance.
About Mavka. We help engineering teams migrate legacy systems without turning every release into a bet: architecture reviews, migration strategy, shadow-testing plans, quality gates, and senior engineers who can finish the hard parts.
Plan a safe monolith migration