“Just rip it out” is the first mistake
Replacing legacy might sound easy. We know how it works, so we can do a big bang: switch it off, drop in the replacement, and everything is better on the other side.
Or not.
If any part of that plan sounds reasonable to you, that is the warning sign. You are underestimating the system, and you are probably about to be in trouble. Both stories below are really about the same thing: how to move deliberately instead.
Story one: from monolith to microservices
When I started out about 20 years ago, the common picture was a few big boxes that integrated by passing files between each other. It worked. It was stable. It was also slow, rigid, and painful to change. The target picture is the familiar one: many smaller services, gateways, clients, databases, and a streaming platform like Apache Kafka in the middle.
Here are the three traps on that journey.
Trap 1: doing microservices because its cool
Wanting to work in a modern style is not a reason. You have to compare what the move actually costs you against what it buys you, for your situation.
Start with the downsides, because they are the ones people skip. Microservices are overhead by nature. Every service needs its own runtime setup, build pipeline, observability, and configuration, and every service needs patching. Patching 50 systems is more work than patching three. You are also building a distributed system, so the network becomes a first-class part of your design: it fails, it adds latency, and you need deliberate strategies for timeouts and retries. You inherit new failure modes, like a flow that is half up because some services are running and some are not, which becomes a real problem the moment that flow needs transactional guarantees. And there is a quieter risk: people build a new service by copying a good existing one, so cross-cutting concerns like authentication, logging, and metrics get duplicated across 50 or 100 repositories unless you have a strategy for them.
The upsides are real too. You get independence: scale the service that is actually under load, deploy pieces individually, lower the risk of each change, and pay only for the capacity you use. You get autonomy, including the ability to keep delivering value while part of the system is down. You get clearer ownership, which is much harder to feel in one giant system. New people understand a focused service faster than a monolith. And you can replace one service without regression-testing the whole estate.
Do this comparison honestly, and only move if the upsides outweigh the downsides for you. Skip it, and design without respecting the downsides, and you end up with a distributed monolith, which has all the overhead of microservices and none of the independence. That is the worst of both worlds.
Trap 2: splitting the monolith badly
The most popular approach is the strangler pattern: instead of a big-bang rewrite, you peel functionality out into smaller services over time, shrinking the monolith as you go. The discipline that makes it work is that every step ships to production. You are not quietly building 500 microservices in a branch and deploying them all on the last day.
For deciding where the seams go, domain-driven design has the right instincts. Set the technology aside and look at the domain first: the information, the processes, how the organization actually works, and split along that.
And start with the foundation. If you know you are going to build 50 services, lay the ground for a factory that can produce them: DevOps practices, platform capabilities, pipelines, observability, and infrastructure as code. Build the assembly line before the cars.
Trap 3: not learning the new toolbox
This environment needs skills the monolith never did. Three worth calling out.
- API gateways. Putting your services behind a gateway is a strong fit for the strangler pattern, because you can keep migrating functionality out without forcing every client to reconfigure each time.
- Asynchronous messaging. A platform like Kafka, plus the know-how to run it, decouples services. Instead of depending directly on the legacy system, services start depending on events. That makes it far easier to lift the legacy out of the dependency graph later.
- Change Data Capture. CDC exposes the changes happening inside your legacy database as an event stream. Rather than calling some aging SOAP service to fetch data, consumers read what actually changed, usually through Kafka. It is one of the cleanest ways to break a hard dependency on a system you are trying to retire.
Story two: from on-prem to cloud
My on-prem experience was mostly a VM mindset. A provider hands you patched VMs. The network has a clear inside and outside. You VPN in, SSH to the machines, and install your workloads with a script or by hand.
Moving to the cloud means moving from a “running applications” mindset to a “data center” mindset. The cloud gives you near-endless freedom in how to solve things, which also means near-endless decisions. On GCP there are at least ten ways to run a workload: GKE, Cloud Run, Cloud Functions, App Engine, and more, each with its own trade-offs. Managed does not mean decided. The cloud does the heavy lifting of infrastructure; it does not make your decisions or write your configuration. Three traps here.
Trap 4: lift and shift
Lift and shift means imaging your VMs and starting them up again on cloud compute. You can do it. It will usually cost more and solve nothing. Move a mess to the cloud and you get a cloud-based mess.
So use the move to look at your systems again. On-prem, an application was a jar or a file you installed on a VM. In the cloud it helps to think wider: an application is a deployment unit plus everything it really needs, like storage, logging, observability, and ingress and egress traffic. That framing positions things correctly. Choose your runtime deliberately rather than dropping the workload wherever is easiest, or you will waste money and miss capabilities that another runtime would have given you for free. Lean on the specialized environments now available, like a managed service for data pipelines (Dataflow) instead of rolling your own. And actually use the elasticity: scale to zero when nothing is happening, burst up when it is, and design applications to scale horizontally by adding instances rather than vertically by adding memory to one VM. Horizontal is the cloud way.
Trap 5: assuming you only pay for what you use
Here is a number that reframes cloud cost. Take every hour in a year. The working hours, eight per day with no weekends or holidays, are about 20 percent of them. So your test and non-production environments, if they run 24/7, sit idle for roughly 80 percent of the time.
The fix is a strategy for automatic teardown and rebuild of non-prod environments. The environment comes up at 8am on a working day and goes away when nobody needs it. It only saves money if it is fully automated, and that automation comes with a bonus: you end up practicing disaster recovery constantly, which is something most teams never rehearse until the day it does not work.
The classic advice still holds. Put alerts on your budget. Tag resources from day one so you can see where the cost actually lives, because once the environment grows you will be staring at an expensive compute line with no idea what is running inside it. You cannot start tagging after five years and expect to catch up. Make it policy from the start.
Trap 6: assuming the cloud is secure by default
Most clouds run on a shared responsibility model: the provider secures the platform, and you secure what you run on it. Take that seriously, because it is a lot of work. Most cloud breaches are not the provider’s fault. They are leaked keys, misconfigured IAM, and buckets left open for anyone to read.
The strongest single piece of advice is to avoid long-lived, static, shareable service account keys. They end up in Slack messages, pipeline logs, and Git repositories, more often than you would like to believe. The pattern is familiar: early on, you create a service account with broad permissions and a key just to get moving, because security is not the fun part at the start, and that little key then lives on for years and never gets cleaned up. There are good ways around it. Workload Identity Federation on GCP uses no keys at all, and short-lived tokens beat static credentials every time.
Zero Trust and least privilege are good directions, but be pragmatic about them. Treat them as a vision rather than an absolute. With Zero Trust, of course you should not leave open doors where everything inside can talk to everything else, but there is a real difference between a service talking to its own authorization server and a service receiving traffic from the internet. With least privilege, GCP exposes something like 12,000 distinct permissions, and you might have hundreds of service accounts and hundreds of human principals to manage. Perfectly minimizing all of that does not scale, because the next feature has you redoing it anyway. Do enough work here, and not more.
One last sneaky monster: pipelines and infrastructure as code. We do not click-ops a whole cloud environment, we define it in code with something like Terraform. Those pipelines tend to run with a very powerful principal, because they have to create, change, and destroy resources. Scope that principal tightly, control who can push code to the pipeline, and remember that if someone compromises the pipeline, you have to treat the whole cloud environment as compromised.
If you want one ratio to set expectations, in my GCP migration work it has been roughly 10 percent of the time building things and 90 percent securing what I built.
The takeaway
Legacy is not the enemy. It is an ATM, a ghost, a pair of handcuffs, and a survivor, all at once, and it is there because it worked. That is exactly why a big bang is the wrong instinct.
Both stories point the same way. Move incrementally with the strangler pattern instead of all at once. Weigh the trade-offs before you commit, so you do not trade a monolith for a distributed monolith or trade an on-prem mess for a cloud one. Decouple with events and CDC so the old system can leave quietly. Use the cloud’s elasticity instead of paying for idle test environments. And secure relentlessly, because in the cloud the breaches are almost always on your side of the shared responsibility line.
If you are partway into one of these migrations, or talking yourself into a big bang, we are happy to compare notes. Untangling legacy is a lot of what our integration and platform teams do, and if you want the longer view on the architecture side, we wrote more about that in From Monoliths to Modern Integration Platforms.