Beyond the Hype: A Pragmatist’s Guide to Migrating from Monolith to Microservices
Every growing tech company reaches a crossroads. The application that once felt lean and agile has become a sprawling monolith, and the siren song of “microservices” grows louder every day. They’re often sold as the holy grail of software development, a cure-all for the pains of scaling.
But what if the cure is worse than the disease? This question is more than theoretical for me. The foundational definition of this architectural style comes from Martin Fowler and James Lewis in their seminal article, “Microservices“. Crucially, they also give the most important piece of pragmatic advice: you shouldn’t start with a microservices architecture. You should only split a monolith once it becomes a clear problem. if the cure is worse than the disease?
I’ve had the unique experience of living through this architectural dilemma from both sides. I’ve guided a US startup away from a painful microservices architecture and back to a clean monolith. I’ve also led the methodical migration of a massive monolithic system for a Serbian telco towards a scalable microservices design.
This isn’t another hype piece. This is a pragmatist’s guide forged from real-world experience. In this post, I’ll share the hard-won lessons from both journeys to help you decide which path is right for you.
Case Study #1: The Reverse Migration – When Microservices Are the Problem
My first eye-opening experience taught me a fundamental truth: microservices solve problems of scale that most small applications simply don’t have. Sometimes, they are the source of the problem.
The Project: An Over-Engineered US Startup
The project was for a small fund-investing startup in the US. With a user base of around 100 people and a few hundred daily orders, it should have been a straightforward application. Instead, they had fallen into the microservices trap early on. When I joined, the application was already built, and they wanted to expand. I was shocked by what I found.
The system was split into six different microservices, each with its own PostgreSQL database, plus MongoDB for logging:
- Admin Service
- User Service
- Order Service
- Fund Service
- Transaction Service
- Notification Service
The Pain: Development Hell
While the tech stack was consistent (Node.js and React), the architecture made development an absolute misery.
- Local Environment Nightmare: To work on any feature, I had to run all six services and their database instances locally. Even with Docker, it was a resource-intensive and fragile setup.
- Cascading Changes: A simple change, like adding a field to a user profile, required modifying and deploying three separate services.
- Data Consistency Chaos: With six separate databases, orphaned records and inconsistent states were a constant headache.
- Complex Testing: Integration testing was a joke. We had to either spin up the entire distributed system or create complex mocks, making reliable testing nearly impossible.
After two months, I was spending more time managing the environment than building features. The client was frustrated. The architecture was killing their development velocity. It was like using a sledgehammer to hang a picture frame.
The Solution: A Bold Move Back to the Monolith
I made a proposal: let’s rebuild this as a clean, modular monolith.
- Frontend: Keep the React app in a separate repository.
- Backend: A single, well-structured Laravel API to handle all business logic.
- Database: A single, normalized PostgreSQL database.
The rebuild took about four months. We consolidated the database schema, built the core Laravel API with a modular internal structure, migrated the data, and connected the React frontend.
The Results: A Dramatic Transformation
- Speed: Tasks that took days now took hours.
- Simplicity: One command started the entire local environment. One deployment pipeline.
- Stability: Debugging became trivial as we could trace the entire request flow in one codebase.
- Performance: Eliminating network calls between services for core operations provided a noticeable boost.
The Core Lesson
The startup’s business model was never going to support millions of users. They had adopted a solution for a problem they didn’t have. By reverting to a well-architected monolith, we aligned the technical complexity with the actual business needs and restored sanity to the development process.
Case Study #2: The “Right Way” Migration – When a Monolith Reaches Its Limits
Starting with a monolith was absolutely the right choice for our project with a major Serbian telco and government partner. It allowed us to move fast and understand a complex business domain before worrying about service boundaries.
The Project: A Massive Toll Collection System
We were building a system to handle highway toll collection and billing for millions of customers across Serbia and neighboring countries. Every transit had to be captured, processed, and billed correctly.
Initially, a single monolithic Laravel application handled everything: customer portals, mobile APIs, third-party integrations, and all core business logic against a single PostgreSQL database. For the first year, it worked beautifully.
The Tipping Point: The Pains of Scale
As the project succeeded, we began to feel the strain.
- Data Volume Explosion: We went from thousands to millions of transits per month. Database queries slowed down.
- Team Bottlenecks: Our team grew from 3 to 10 developers. Merge conflicts and risky, coordinated deployments became the norm.
- Conflicting Release Cycles: The admin app, web app, and third-party APIs all needed to be updated on different schedules, which was a logistical nightmare in a single application.
- Performance Divergence: The real-time admin interface had different performance needs than the batch-oriented billing engine, which could take hours to run and bog down the system.
The Solution: A Strategic, Gradual Split
We decided to migrate, but methodically and for the right reasons. Our service boundaries were based on business domains, not just technical layers.
First, We Cleaned the House: The Modular Monolith
Before writing a single new service, we spent two months refactoring our monolith. We moved from a standard MVC structure to a Domain-Driven one, isolating code into folders like app/Customer and app/Transit. These “domains” behaved like services but still lived in the same codebase. This step alone made the system dramatically easier to reason about and prepared it for the split.
Then, We Applied the Strangler Fig Pattern
We used the classic Strangler Fig Pattern, a concept detailed by Martin Fowler, to gradually carve off pieces of functionality without downtime.
- We started by separating the customer-facing Web/Mobile API from the internal Admin App.
- Next, we carved out the Integration Service for third-party APIs. Initially, it shared the same database but had its own server and queue system, immediately offloading a major source of traffic.
- The most complex part was extracting the Billing Service into a true microservice with its own dedicated database, using APIs for communication.
- Finally, we isolated long-running background processes into their own service, freeing up the Admin App to be a pure interface.
The Results: A System Built to Last
- Independent Deployments: Teams could now deploy their services on their own schedules.
- Targeted Scaling: We could scale the Transit Processing service independently of the Billing service.
- Team Autonomy: Small, focused teams owned their services from end to end.
- Improved Maintainability: The codebase became easier to navigate and understand.
The Core Lesson
We only split the monolith when it actually hurt. The migration was driven by clear business needs and based on business domains. We learned that microservices require a serious investment in infrastructure, but when applied correctly to a complex, large-scale problem, the benefits are immense.
Conclusion: The Pragmatist’s Take
Microservices aren’t inherently good or bad—they’re a tool. The hype has led many teams to apply this powerful tool to the wrong problem.
My advice is simple:
- Start with a clean, well-structured monolith. Focus on good software engineering principles like modularity and Domain-Driven Design from day one.
- Split into microservices only when you have clear, painful business problems that a distributed architecture can solve.
- If you know from the beginning that your application will definitely operate at a massive scale—millions of users, complex integrations, large teams—then and only then should you consider a microservices-first approach.
The goal isn’t to have microservices. The goal is to have a system that is maintainable, scalable, and delivers value to your users. Sometimes that means a suite of small services, and sometimes it means a beautifully architected monolith.
What’s your experience been? Have you navigated this complex choice? I’d love to hear your stories in the comments below.