Technology

Microservices vs Monolith: I Migrated 3 Production Apps and Here’s What Actually Broke

15 min read
Technologyadmin18 min read

Three years ago, I made the decision that would consume the next 18 months of my engineering life: migrating our production applications from monolithic architectures to microservices. The promise was seductive – independent deployments, better scalability, technology flexibility. What actually happened? Two complete system outages, a 340% increase in infrastructure costs during the transition, and enough debugging nightmares to fill a horror anthology. But here’s the thing nobody tells you about microservices vs monolith migration: the theoretical benefits everyone talks about are real, but the path to getting there is paved with production incidents that will test every ounce of your patience and skill.

I’m not writing this to discourage you from migrating. I’m writing this because when I started, every blog post I read made it sound straightforward. They talked about domain boundaries and service meshes but conveniently skipped over the part where your authentication system breaks at 3 AM because of a circular dependency you didn’t know existed. Over the course of three migrations – an e-commerce platform, a SaaS analytics tool, and a customer support application – I learned more about distributed systems than any computer science course ever taught me. The lessons were expensive, sometimes embarrassing, but ultimately invaluable. This is the unvarnished truth about what breaks when you make the leap from monolith to microservices.

The Three Applications I Migrated (And Why Each One Was Different)

Let me give you the context before we dive into the failures. The first application was an e-commerce platform handling about 50,000 transactions daily. The monolith was a Ruby on Rails application that had grown to 180,000 lines of code over five years. Database queries were becoming sluggish, and deployments required 45-minute maintenance windows that our customers absolutely hated. We started this migration in January 2021, and it took 11 months to complete. The second application was a SaaS analytics dashboard built in Node.js with about 12,000 active users. This one was actually performing reasonably well, but we wanted to add real-time features that the monolithic architecture made difficult. This migration took 6 months and was the smoothest of the three.

The third application was a customer support platform with ticketing, chat, and knowledge base functionality. This Python Django monolith was the most complex – it had tight coupling between every feature, and the database schema was a tangled mess of foreign keys that made extraction feel like performing surgery with a butter knife. This migration stretched to 14 months and taught me more about what not to do than the other two combined. Each application taught different lessons about microservices vs monolith migration challenges, and the problems that surfaced were rarely the ones I anticipated.

Application Complexity Metrics That Mattered

Looking back, I should have paid more attention to specific metrics before starting. The e-commerce platform had 47 database tables with an average of 8.3 relationships per table. The analytics tool had only 23 tables but processed 2.1 million events per day. The support platform had 89 tables with some having over 20 foreign key relationships. These numbers directly correlated with migration difficulty. The more interconnected your data model, the harder it becomes to establish clean service boundaries. I spent weeks just mapping dependencies and creating service boundary proposals that got revised repeatedly as we discovered hidden coupling.

Team Size and Skill Distribution

Our team composition also varied across migrations. For the e-commerce platform, I had six engineers with mixed experience in distributed systems. The analytics migration had four engineers, two of whom had worked with microservices before. The support platform migration started with five engineers but grew to nine as we realized the scope. Having engineers with prior microservices experience made an enormous difference – they caught issues in design reviews that would have become production incidents later.

What Actually Broke: The Authentication Nightmare

The first major failure hit us three weeks into the e-commerce migration. We had successfully extracted the product catalog service and felt pretty good about ourselves. Then we tried to extract the user authentication service. Our monolith used session-based authentication with server-side session storage. Simple enough, right? We’d just move that logic to its own service and have other services call it. What we didn’t account for was the cascade of issues this created.

First problem: latency. Every single request to any service now required an authentication check against the auth service. What was previously an in-memory session lookup suddenly became a network call. Our average response time jumped from 120ms to 380ms overnight. Users noticed immediately. We scrambled to implement JWT tokens to reduce authentication calls, but that introduced its own problems. Token invalidation became nearly impossible – if we needed to force-logout a user, we couldn’t actually invalidate their JWT without building a distributed blacklist system. We ended up implementing Redis-based token storage with short expiration times, essentially recreating the session system we tried to replace but now with network overhead.

The Circular Dependency We Didn’t See Coming

The second authentication disaster was more insidious. Our user service needed to authenticate requests, so it called the auth service. But the auth service needed to fetch user details to validate permissions, so it called the user service. Circular dependency. The services would hang indefinitely waiting for each other during certain operations. This didn’t surface in testing because our test scenarios didn’t trigger the specific code paths that caused the loop. It only appeared in production when a user with specific permission settings tried to update their profile. The fix required careful redesign of which service owned which data and introducing eventual consistency in places we originally wanted strong consistency.

Session Affinity and Load Balancer Hell

We also discovered that our load balancer configuration, which worked perfectly for the monolith, was completely wrong for microservices. Session affinity was routing all requests from a user to the same instance of each service, which defeated the purpose of horizontal scaling. When we disabled session affinity, we found race conditions in our code that had been masked by single-instance behavior. Fixing these race conditions required implementing distributed locks using Redis, adding another point of failure to our system. The complexity snowballed faster than we could document it.

Database Transactions Across Service Boundaries

Here’s where the microservices vs monolith migration really got painful. In the monolith, we had beautiful ACID transactions. Creating an order involved updating inventory, creating an order record, charging a payment method, and sending confirmation emails – all wrapped in a single database transaction. If anything failed, everything rolled back cleanly. When we split these operations across services (inventory service, order service, payment service), we lost that transactional guarantee.

Our first attempt at solving this was implementing a saga pattern with choreography – each service would publish events, and other services would react to those events. This created a nightmare of eventual consistency issues. A customer would place an order, see a confirmation screen, then get an email 30 seconds later saying the order failed because inventory wasn’t actually available. The inventory service had processed its check asynchronously and found insufficient stock after the order service had already confirmed success. Customer support tickets tripled during the first week of this implementation.

The Two-Phase Commit That Wasn’t

We tried implementing a two-phase commit protocol next, thinking we could maintain strong consistency. This was theoretically sound but practically disastrous. The coordinator service became a single point of failure, and network partitions would leave transactions in limbo. We’d have orders that were half-completed – payment charged but inventory not decremented, or inventory reserved but payment never processed. The cleanup logic for handling these partial failures grew more complex than the original transaction logic in the monolith. We spent three weeks just writing compensating transactions and testing failure scenarios.

Eventual Consistency: The Compromise We Learned to Live With

Eventually (pun intended), we accepted eventual consistency for most operations. We implemented an orchestration-based saga using Temporal workflow engine, which gave us better visibility into long-running transactions and automatic retries. This worked much better, but it required a fundamental shift in how we thought about data consistency. We had to add idempotency keys to every operation, implement proper event sourcing for critical workflows, and build monitoring dashboards that could show us the state of in-progress sagas. The operational complexity increased by an order of magnitude, and we needed dedicated engineers just to maintain this infrastructure. For teams considering this migration, understand that technical debt in distributed transaction handling can accumulate faster than in any other area.

Monitoring and Debugging Became a Full-Time Job

In the monolith, debugging was relatively straightforward. You’d look at the logs, find the error, trace through the stack trace, and identify the problem. With microservices, a single user request might touch 12 different services. When something went wrong, we’d have logs scattered across multiple services, each with their own timestamp that might be slightly out of sync. Correlating these logs to understand what actually happened was like trying to reconstruct a conversation by reading 12 different people’s diary entries.

We initially tried using ELK stack (Elasticsearch, Logstash, Kibana) for centralized logging. Setting it up took two weeks, and it worked okay for simple queries. But when we had a production incident at 2 AM and needed to trace a specific user’s request through the system, Elasticsearch queries were too slow and too complex. We’d be writing nested JSON queries while the system was down and customers were angry. Not ideal. We eventually moved to Datadog APM, which cost us $2,300 per month but saved countless hours of debugging time. The distributed tracing feature alone was worth the price – we could see the entire request flow with timing information for each service call.

The Incident That Took 6 Hours to Debug

One memorable incident involved intermittent 500 errors affecting about 3% of requests. No pattern to which requests failed. No obvious errors in any service logs. We spent six hours checking every service, reviewing recent deployments, analyzing database performance. The culprit? A network timeout configuration on one service that was set to 1 second. Under normal load, all requests completed in under 800ms. But when load spiked slightly, some requests would take 1.1 seconds and timeout silently. The service making the request didn’t log the timeout properly, and the receiving service never saw the request complete, so it didn’t log anything either. We only found it by adding detailed timeout logging to every service and reproducing the issue under load. This kind of debugging was completely unnecessary in the monolith.

Observability Infrastructure Costs

The monitoring infrastructure itself became a significant cost center. Between Datadog APM, Prometheus for metrics, Grafana for dashboards, and PagerDuty for alerting, we were spending $4,800 per month on observability tools alone. That’s not counting the engineering time spent configuring and maintaining these systems. In the monolith days, we spent maybe $200 per month on basic application monitoring. The microservices architecture demanded this investment – without proper observability, you’re flying blind.

Infrastructure Costs Exploded (Then Stabilized)

Let’s talk money. Our monolith ran on three EC2 instances (one primary, two replicas) plus a managed PostgreSQL database. Monthly AWS bill: $890. During the microservices migration, costs ballooned to $3,950 per month at peak. Why? Each service needed its own compute resources, load balancer, and often its own database. We had 14 services by the end of the e-commerce migration, each running on at least two instances for redundancy. Add in the service mesh (we used Istio), API gateway (Kong), message queue (RabbitMQ), and caching layer (Redis Cluster), and the infrastructure complexity was staggering.

The cost story did improve after the migration completed and we optimized. We consolidated some services that were too granular, implemented auto-scaling properly, and moved to containerized deployments with Kubernetes that allowed better resource utilization. Final steady-state cost: $2,100 per month. Still more than double the monolith cost, but the improved deployment velocity and ability to scale individual components independently did provide value. However, for the analytics application, which was performing fine as a monolith, the cost increase was harder to justify. We went from $450 per month to $1,200 per month with minimal business benefit beyond the technical satisfaction of having a more modern architecture.

The Hidden Costs Nobody Mentions

Beyond infrastructure, there were hidden costs that caught us off guard. Developer productivity dropped during the migration as engineers context-switched between services and dealt with integration issues. Our deployment pipeline needed complete redesign to handle multiple services with dependencies. We had to implement feature flags (using LaunchDarkly at $500/month) to enable gradual rollouts across services. The learning curve for new engineers joining the team increased significantly – onboarding now required understanding distributed systems concepts, not just the application domain.

When Does the Cost Make Sense?

Looking at our three migrations, the cost premium made sense for the e-commerce platform, which genuinely needed independent scaling of different components. During Black Friday, we could scale up the checkout service without scaling the product catalog service. For the analytics application, the cost increase was questionable – we probably could have achieved our goals with a modular monolith approach. For the support platform, the jury’s still out. We gained deployment independence, but the operational complexity might not be worth it for a team of our size.

How Should You Actually Decide Between Microservices and Monolith?

After living through three migrations, I’ve developed a framework for making this decision. It’s not about whether microservices are better than monoliths in the abstract – it’s about whether the specific benefits of microservices solve problems you actually have. Start by honestly assessing your pain points. Are deployments actually painful because unrelated changes conflict? Or are they painful because you lack proper CI/CD and testing infrastructure? If it’s the latter, microservices won’t help – they’ll make it worse.

Do you have truly independent scaling needs? The e-commerce platform’s checkout service needed 5x more resources during peak times than the product catalog service. That’s a real scaling problem that microservices solve elegantly. But if your entire application scales together as a unit, horizontal scaling of the monolith works fine and is much simpler. Consider your team size and structure. Conway’s Law is real – your architecture will mirror your organization structure. If you have three teams working on different features with minimal overlap, microservices can enable independence. If you have six engineers all working across the entire codebase, microservices will create artificial barriers that slow everyone down.

The Modular Monolith Alternative

Here’s what I wish someone had told me before the first migration: consider a modular monolith first. You can achieve many benefits of microservices – clear boundaries, independent development, testability – within a monolithic deployment. Use proper module boundaries, dependency injection, and interface-based design. The Shopify engineering team runs one of the largest Rails monoliths in existence and has written extensively about making it work at scale. This approach gives you the option to extract services later if you genuinely need to, but you avoid the operational complexity until it’s actually necessary.

Team Maturity Requirements

Microservices demand a level of operational maturity that many teams don’t have. You need solid CI/CD pipelines, comprehensive monitoring, automated testing at multiple levels, and engineers who understand distributed systems concepts. If you’re still struggling with basic deployment automation or don’t have proper staging environments, fix those problems first. Microservices will amplify your existing operational weaknesses, not solve them. Our analytics team had strong DevOps practices, which is why that migration went relatively smoothly. The support platform team was still learning, and it showed in the migration timeline and incident rate.

What I’d Do Differently Next Time

If I had to do these migrations again, I’d make several changes. First, I’d start with a strangler fig pattern more aggressively. Instead of trying to plan the entire service architecture upfront, I’d extract one service completely, run it in production for three months, and learn from that experience before extracting the next one. We tried to extract multiple services in parallel to speed up the migration, but this meant we repeated the same mistakes across multiple services before learning better patterns. Sequential extraction would have been slower but ultimately more efficient.

Second, I’d invest heavily in contract testing from day one. We added contract tests (using Pact) halfway through the e-commerce migration after several integration failures. This should have been part of the initial infrastructure. Contract testing catches interface mismatches between services before they reach production, and it enables teams to work more independently. The time spent setting up contract testing infrastructure pays back within weeks. Third, I’d be more conservative about service boundaries. We extracted some services that were too small, creating unnecessary network overhead for operations that belonged together. The order validation service and order creation service should have been a single service – they were always called together and shared the same data model.

The Importance of Feature Flags

Feature flags should be non-negotiable for microservices migration. We implemented them late in the process, but they should have been there from the start. Being able to route traffic between the old monolith code path and the new microservice code path dynamically, without redeployment, is essential for safe migration. When we found issues with the new microservice implementation, we could instantly fall back to the monolith behavior while we fixed the problem. This reduced the blast radius of our mistakes significantly. Similar to how deploying on Friday requires safety nets, migrating to microservices demands even more robust rollback mechanisms.

Documentation and Runbooks

We massively underinvested in documentation during the migrations. Each service needs a runbook explaining what it does, how to deploy it, common failure modes, and how to debug issues. We created these reactively after incidents, but they should have been created proactively as part of the migration. When you’re on-call at 3 AM and a service you didn’t write is failing, good documentation is the difference between a 15-minute fix and a 3-hour debugging session. We also needed better architecture decision records (ADRs) documenting why we made specific design choices. Six months later, when someone questioned a decision, we often couldn’t remember the reasoning.

Is Microservices vs Monolith Migration Worth It?

So after all this – the outages, the cost increases, the debugging nightmares – was it worth it? The answer is frustratingly nuanced. For the e-commerce platform, yes. We now deploy individual services 3-4 times per day without affecting other parts of the system. When the recommendation engine team wants to experiment with a new algorithm, they can deploy independently without waiting for the checkout team’s release schedule. During peak traffic, we scale only the services that need it, which actually reduced costs compared to scaling the entire monolith. The system is more resilient – when the review service went down last month, the rest of the site continued functioning normally.

For the analytics application, I’m less convinced. We achieved our goal of adding real-time features, but we probably could have done that with a well-architected monolith using WebSockets and a message queue. The operational complexity we added doesn’t feel proportional to the benefits we gained. The team spends more time on infrastructure concerns and less time on features that matter to users. For the support platform, it’s too early to tell. The migration finished six months ago, and we’re still dealing with occasional issues related to the distributed architecture. The independent deployment capability is nice, but the system feels more fragile than the monolith did.

The Real Value Proposition

The true value of microservices isn’t in the architecture itself – it’s in the organizational benefits when you have the right structure to support it. If you have multiple teams that need to move independently, if you have genuinely different scaling requirements for different components, if you need to use different technology stacks for different problems, then microservices enable those things. But these are organizational and business problems, not technical ones. If you don’t have these problems, you’re adding complexity without corresponding benefit. The best advice I can give: be honest about which problems you’re actually solving, and be prepared for the problems you’ll create in the process.

The Learning Curve Is Real

One final consideration: the learning experience itself has value. Our engineering team is now much more sophisticated about distributed systems, eventual consistency, observability, and operational excellence. These are valuable skills in the current software landscape. But you’re essentially paying for education with production stability and engineering time. Make sure that tradeoff makes sense for your organization. If you’re a startup trying to find product-market fit, investing heavily in microservices architecture is probably premature optimization. If you’re a growing company hitting real scaling walls, it might be exactly the right investment.

The best architecture is the one that solves your actual problems without creating disproportionate new ones. Sometimes that’s microservices. Often, it’s not.

References

[1] Martin Fowler – Microservices: A definition of this new architectural term discussing the key characteristics and tradeoffs of microservices architecture.

[2] ACM Queue – Building Microservices: Designing Fine-Grained Systems – Technical analysis of microservices design patterns and anti-patterns from industry practitioners.

[3] IEEE Software – The Hidden Costs of Microservices: Complexity, Operations, and Organizational Overhead – Research study quantifying the operational costs of microservices adoption.

[4] Communications of the ACM – Lessons from Giant-Scale Services: A detailed examination of distributed systems challenges at scale from major technology companies.

[5] Journal of Systems and Software – Empirical Study of Microservices Migration Patterns – Academic research on common patterns and pitfalls in microservices migrations.

admin

About the Author

admin

admin is a contributing writer at Big Global Travel, covering the latest topics and insights for our readers.