Software Development Services

Migrating Monoliths to Cloud-Native: What Actually Works

Vivek Dutt
September 3, 2026

Editor’s Note: Cloud-native migration is a sequencing problem. This piece sets out the order we'd follow on any monolith: routing first, then observability, then state out of the old runtime, and storage split last. Migrations we've run show what each step buys and what it costs to skip. If your team is planning a similar move, learn how Covalience's legacy application modernization and infrastructure management services can help.

Migrating a monolith to cloud-native architecture is a sequencing problem. Decide the order in which pieces move, make each move reversible, and the target architecture will mostly take care of itself.

I have worked on three of these. One kept the monolith and changed only how it was deployed. The other two replaced code, one off PHP 5 and one off a set of Perl processing scripts. All three worked once we ran them as a controlled change program, with a rollback path at every step.

Start with the business reason for the migration

A cloud-native migration needs a business reason: security, release speed, reliability, cost control, hiring difficulty, or the need to scale one part of the system independently. If the reason is only “modernization,” the project will struggle when it hits the first budget review.

The rehosting project was driven by reliability. The PHP version was past end of support, the hosting was unstable, and outages were reaching users. The client wanted a platform that stayed up and said so in those terms.

In the PHP 5 rewrite, the reason was simple. The runtime was past end of life and the application still carried real business traffic. Staying on it meant accepting vulnerabilities we could no longer patch.

Best practice 1: Start with the paths you can redirect

Sequencing decides whether a migration lands. A bad sequence turns a good target architecture into a long-running support burden.

Start at the seams. A seam is any point where you can send a request to different code without the user noticing. Read-only features are usually the safest first move. Clear input and output boundaries come next. Shared session state, heavy writes, and tangled business rules should come later.

A router should be in front of the application before the first extraction. API Gateway, an Application Load Balancer, CloudFront routing, or Cloud Load Balancing can all work. The point is simple: move a small slice of traffic, watch it, and move it back fast if needed.

Best practice 2: Move state before moving services

State is what makes monolith migrations stall. Most teams find the code easier to move than the sessions, files, schedules, and database access underneath it.

Local sessions, files written to disk, cron jobs on one server, and direct database access from every module are the quiet anchors that keep a monolith tied to its old shape. Removing those anchors early creates room to migrate safely.

Do not rush to split the database on day one. Logical ownership should come before physical separation. Let one service own a group of tables. Make other services go through its interface. Watch that boundary under real traffic before splitting storage.

Best practice 3: Match the runtime to the workload

Most monoliths contain several workload types inside one deployment. Short request-response operations, long-running reports, scheduled jobs, background processing, and bulk imports often live together because the old system had only one place to put them.

Those workloads belong on different targets. Functions handle short, spiky requests well. Containers suit long-running work and anything with native dependencies, because you control the runtime. Fan-out processing needs a queue and a pool of workers, and scheduled jobs belong on a managed scheduler, or at least on an instance of their own rather than on the web server.

Migration 1: PHP monolith rehosted on Docker and EC2 without a rewrite

Starting point: PHP at end of support on unstable hosting

The application was a PHP monolith running on hosting that had become unstable, and the PHP version it depended on was past end of support. Outages were reaching end users. The client wanted the platform stable and supportable and did not want the application rewritten.

Step 1: Move the PHP runtime forward

We upgraded the PHP version first, because everything else depended on running on a supported runtime. No feature was rewritten. The application kept its structure and its business logic.

Step 2: Containerize the application and move the database out

The application was packaged as a Docker image and run on EC2, which made the environment reproducible and took the hand-configured server out of the picture. The database moved to RDS, so backups, patching and failover stopped being the team's responsibility.

Step 3: Take the heavy work off the user-facing servers

Cron jobs moved to their own EC2 instance and report queries moved to a second one. An admin running a six-month sales analysis, or generating a month of invoices, had been slowing the site for every other user. Once that work ran elsewhere it stopped competing with normal traffic.

Step 4: Put a CDN and a cache in front

Cloudflare took over content delivery, so fewer requests reached the application at all. A self-managed Redis cache went in front of the reads the application was repeating on every page.

Step 5: Restrict the admin panel to the office VPN

The admin section performs actions that are expensive to get wrong, and the client has ten to fifteen people who use it. Limiting access to the office VPN was cheap to implement at that headcount and took the admin panel off the public internet.

Step 6: Add observability and tracing

We added observability and request tracing across the application, and set up monitoring on the parts that had been failing quietly. This is what turned "the site feels slow" into a specific slow query or a specific container.

What went wrong

The PHP version bump had breaking changes, so several areas of code needed small updates before they would run. Nothing was redesigned, but "no code changes" was not an accurate description of the work, and a version jump of this size is worth budgeting for.

Migration 2: PHP 5 to Node.js microservices on AWS serverless

Starting point: PHP 5 on Apache

The application was a PHP 5 codebase running on Apache with mod_php. Business logic and page rendering were mixed in the same files. Sessions lived on local disk. Cron jobs ran on the web server. One MySQL database served the whole application.

Step 1: Put a router in front

We placed CloudFront and API Gateway in front of the existing application. That gave us traffic control. Once the router existed, we could move specific paths to the new services instead of trying a large cutover.

Step 2: Move session state out of the web server

Local file sessions were replaced with a shared session approach using DynamoDB and signed tokens. This one change gave the old application more life. Any instance could serve any request, which made scaling and rollback much cleaner.

Step 3: Extract the read-only endpoints

We extracted read-only catalog and reporting endpoints first. These became Node.js services running on Lambda, routed through path rules. We deliberately avoided the central write-heavy flows at the start, because a read-only service can be rolled back within minutes while a bad write path can leave damaged data behind.

Step 4: Move the frontend module by module

The pages moved the same way, one module at a time. Each module's screens and the backend services behind them were migrated together, so a module was either fully on the old stack or fully on the new one, and the CloudFront path rules decided which. That kept the rollback unit small, because a module that misbehaved could be pointed back at PHP without touching the modules already moved.

Step 5: Move workflows and scheduled jobs

Multi-step PHP scripts became Step Functions workflows. This helped more than expected because each step became visible. Earlier, a failed script meant reading logs and guessing where the failure happened. With a state machine, retries, failures, and progress were easier to see.

Cron jobs moved to EventBridge scheduled rules. The web server stopped carrying hidden operational responsibility. The database moved to RDS, but we kept it shared for most of the migration while enforcing table ownership through service-level data access modules.

What went wrong

Lambda cold starts were visible on critical paths, so we trimmed dependencies and used provisioned concurrency only where users would feel the delay. Lambda database connections exhausted MySQL until we added RDS Proxy. Functions inside a VPC added latency, so we avoided that unless the data access pattern required it.

The order I would follow on any monolith migration

  • Make the routing reversible before you move anything, whether that means HTTP paths or the dispatch of a batch job. Every step after this one depends on being able to put traffic back.
  • Get observability and tracing in before you change anything, so a problem shows up as a specific query or container rather than as a support ticket saying the site feels slow.
  • Move state out of the old runtime early: sessions, files written to disk, and anything scheduled on a single machine.
  • Extract the lowest-risk slice first and leave it in production for a full release cycle before extracting a second. The first extraction shows you what your deployment and monitoring are missing.
  • Prove behavior before you cut over. Capture real inputs and outputs from the old system and make the new one reproduce them.
  • Run both systems in parallel through a full business cycle whenever the old behavior is hard to specify.
  • Split storage last, after one service has owned its data through a full business cycle.

About the Author

Vivek Dutt
Dev Manager - Emerging Technologies
Covalience
Vivek Dutt is Development Manager - Emerging Technologies at Covalience, where he helps the organization design scalable solutions and bring emerging technologies into real-world delivery. With over 17 years in software engineering, he works across cloud platforms, application modernization, and AI adoption — translating technical possibility into practical business outcomes. His focus areas include AI strategy, solution architecture, and engineering excellence.

Legacy App Modernization Services

Don't let legacy systems cap your growth.
From rehosting to full rebuilds, we modernize aging applications into secure, scalable platforms — freeing budget and engineering capacity that legacy systems consume.
Explore Services
On this page
Have a question?
Ask our experts
Schedule A Call

Frequently Asked Questions

How long should a monolith migration take?
Should we rewrite or refactor?
Do we need microservices to be cloud-native?
When is serverless the wrong choice?