DEV Community

Cover image for From Monolith to Serverless: How We Cut Infrastructure Costs by 30–35%:
Manohar Halappa
Manohar Halappa

Posted on

From Monolith to Serverless: How We Cut Infrastructure Costs by 30–35%:

From Monolith to Serverless: How We Cut Infrastructure Costs by 30–35%

Modernizing a legacy monolith is rarely a matter of replacing servers with Lambda.

The difficult part is deciding what should become serverless, what should remain where it is, and in what order to make the change without disrupting the business.

In two modernization programs, I found that the biggest gains came from treating modernization as an architecture and economics problem, not simply a technology migration.

The result was a 30–35% reduction in infrastructure costs for the workloads we migrated.

The interesting part wasn't Lambda itself.

It was changing the system from:

Always running
      ↓
Wait for work
      ↓
Process work
      ↓
Wait again
Enter fullscreen mode Exit fullscreen mode

to:

Event arrives
      ↓
Compute starts
      ↓
Process
      ↓
Compute stops
Enter fullscreen mode Exit fullscreen mode

The fundamental shift was from paying for capacity to paying for useful work where the workload characteristics made that model appropriate.


Start With Boundaries, Not Code

One of the easiest mistakes in modernization is starting with:

"Which class should we extract first?"

That's usually the wrong question.

Start with:

"Which business capability has a clear boundary and a workload that benefits from independent scaling?"

Before touching the code, map the existing system.

Look at:

  • Request volume
  • Traffic patterns
  • CPU and memory utilization
  • Processing duration
  • Dependency relationships
  • Database access patterns
  • Failure characteristics
  • Scaling behavior
  • Deployment frequency
  • Business criticality

The resulting workload map often looks something like this:

                       Workload Characteristics

              ┌───────────────────────────────────┐
              │ High volume + predictable traffic  │
              │                                   │
              │ Existing service may be suitable │
              │ for optimized container compute  │
              └───────────────────────────────────┘

              ┌───────────────────────────────────┐
              │ Spiky + stateless + short-lived   │
              │                                   │
              │ → Lambda / API Gateway           │
              └───────────────────────────────────┘

              ┌───────────────────────────────────┐
              │ Async + long-running              │
              │                                   │
              │ → SQS + Step Functions + Lambda  │
              └───────────────────────────────────┘

              ┌───────────────────────────────────┐
              │ Large batch / data processing     │
              │                                   │
              │ → S3 + event-driven processing   │
              └───────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

This exercise prevents one of the most common modernization mistakes:

Forcing every workload into the same architecture.

Serverless is a tool, not an ideology.


The Extraction Strategy

Rather than rewriting the monolith, we extracted specific capabilities behind well-defined interfaces.

A simplified evolution looked like this:

                    BEFORE

              ┌─────────────────┐
              │                 │
              │    MONOLITH     │
              │                 │
              │  API            │
              │  Business Logic │
              │  Batch Jobs     │
              │  Integrations   │
              │                 │
              └───────┬─────────┘
                      │
                   Database
Enter fullscreen mode Exit fullscreen mode

Over time:

                    AFTER

                ┌──────────────┐
                │ API Gateway  │
                └──────┬───────┘
                       │
                  ┌────▼─────┐
                  │  Lambda  │
                  └────┬─────┘
                       │
                       ▼
                ┌─────────────┐
                │  Services   │
                └─────────────┘

 Events ──→ SQS ──→ Lambda ──→ DynamoDB/S3

 Events ──→ Step Functions
                 │
          ┌──────┼──────┐
          ▼      ▼      ▼
       Lambda  Lambda  Lambda
Enter fullscreen mode Exit fullscreen mode

The monolith didn't disappear overnight.

Instead, capabilities were progressively moved behind explicit boundaries.

That reduced both technical and organizational risk.


1. Read-Heavy and Spiky Workloads

The first candidates were workloads with:

  • Highly variable traffic
  • Short execution times
  • Stateless processing
  • Clear API boundaries
  • Significant idle periods

These workloads are natural candidates for:

API Gateway → Lambda → downstream service

The economics can be attractive because capacity scales with demand rather than requiring permanently provisioned application servers.

Consider a simplified workload:

Traffic

       ▲
       │             ████
       │             ████
       │       ███   ████
       │       ███   ████
       │  ██   ███   ████
       │  ██   ███   ████
       └──────────────────────→ time
Enter fullscreen mode Exit fullscreen mode

With always-on infrastructure, you provision for the peak:

Provisioned capacity ─────────────────────
Actual workload        ▂▂▃▂▂▆▃▂▂▂▂▇▂
Enter fullscreen mode Exit fullscreen mode

That gap represents potentially unused capacity.

With serverless:

Provisioned capacity
Actual execution      ▂▂▃▂▂▆▃▂▂▂▂▇▂
Enter fullscreen mode Exit fullscreen mode

The infrastructure model more closely follows actual execution.

But there is an important qualification:

Serverless isn't automatically cheaper.

For continuously high utilization, long-running workloads, or workloads with substantial execution duration, other compute models can be more economical.

The architecture decision should therefore be based on workload economics, not technology preference.


2. Replace Polling With Events

One of the biggest opportunities wasn't moving existing code to Lambda.

It was eliminating unnecessary work.

A common legacy pattern looked like:

┌──────────────┐
│ Always-on VM │
└──────┬───────┘
       │
       ▼
   Poll queue
       │
       ├── No work → sleep
       │
       └── Work → process
Enter fullscreen mode Exit fullscreen mode

The system was effectively paying for compute even when there was nothing to process.

We moved appropriate workflows toward:

                 Event
                   │
                   ▼
                  SQS
                   │
                   ▼
                Lambda
                   │
                   ▼
               Processing
Enter fullscreen mode Exit fullscreen mode

Now compute existed primarily when there was work to perform.

This wasn't just a cost optimization.

It also reduced coupling between producers and consumers and made scaling more explicit.


3. Long-Running Workflows: Step Functions

Not every workflow belongs inside a single Lambda invocation.

Some operations involve multiple stages:

Fetch
  ↓
Validate
  ↓
Transform
  ↓
Persist
  ↓
Notify
Enter fullscreen mode Exit fullscreen mode

Putting all of that into one function creates problems around:

  • Timeout limits
  • Retry behavior
  • Partial failure
  • Observability
  • Recovery
  • Operational visibility

Instead, we modeled long-running workflows explicitly.

For example:

             ┌────────────┐
             │   Start    │
             └─────┬──────┘
                   ▼
             ┌────────────┐
             │   Fetch    │
             └─────┬──────┘
                   ▼
             ┌────────────┐
             │  Validate  │
             └─────┬──────┘
                   ▼
             ┌────────────┐
             │ Transform  │
             └─────┬──────┘
                   ▼
             ┌────────────┐
             │   Persist  │
             └─────┬──────┘
                   ▼
             ┌────────────┐
             │  Complete  │
             └────────────┘
Enter fullscreen mode Exit fullscreen mode

The workflow engine became responsible for orchestration, while individual functions remained focused on individual tasks.

This made failures easier to reason about.

If Transform fails, the workflow knows exactly where it failed and what retry policy applies.


4. Bulk Data Processing

Large data operations required a different approach.

Instead of treating a massive batch as one application process, we used object storage and event-driven processing where appropriate.

A typical pattern was:

                 ┌─────────────┐
                 │     S3      │
                 │ Raw Dataset │
                 └──────┬──────┘
                        │
                     Event
                        │
                        ▼
                 ┌─────────────┐
                 │ Processing  │
                 │   Workers   │
                 └──────┬──────┘
                        │
                        ▼
                 ┌─────────────┐
                 │  Persisted  │
                 │    Data     │
                 └─────────────┘
Enter fullscreen mode Exit fullscreen mode

This creates another useful separation:

Storage doesn't need to be coupled to compute.

Data can exist independently of the processing lifecycle.

That makes retries, replay, and recovery significantly easier to design.


5. The Cost Reduction Was a Consequence, Not the Architecture

This is an important distinction.

We didn't say:

"Let's move everything to Lambda and save money."

We asked:

"Where are we paying for capacity that isn't being used?"

Then we targeted those workloads.

The cost model changed from:

Always-on infrastructure
+
Provisioned capacity
+
Idle periods
+
Operational overhead
Enter fullscreen mode Exit fullscreen mode

toward:

Actual execution
+
Event-driven scaling
+
Managed orchestration
Enter fullscreen mode Exit fullscreen mode

For the workloads we migrated, this contributed to a 30–35% infrastructure cost reduction.

But the number should not be interpreted as a universal serverless savings percentage.

The actual economics depend on:

  • Invocation frequency
  • Execution duration
  • Memory allocation
  • Concurrency
  • Data transfer
  • Storage
  • Database costs
  • Observability costs
  • Existing infrastructure utilization
  • Provisioned versus on-demand capacity

That's why cost per business transaction was more useful than looking only at the monthly infrastructure bill.


6. Guardrails Before Migration

Moving from a monolith to distributed services increases the number of things that can fail.

We therefore treated guardrails as part of the architecture.

Rollback-first deployments

Every deployment needed a safe rollback path.

The question wasn't:

"Can we deploy this?"

It was:

"What happens if this deployment is wrong?"

A deployment isn't production-ready until the recovery path is understood.


Infrastructure as Code

We standardized infrastructure using tools such as:

  • Terraform
  • AWS CDK
  • GitHub Actions

The goal wasn't simply automation.

It was repeatability.

A service shouldn't require an individual engineer to remember a collection of undocumented console steps.

Instead:

Code
  ↓
Pull Request
  ↓
Validation
  ↓
Infrastructure Plan
  ↓
Deployment
Enter fullscreen mode Exit fullscreen mode

The infrastructure becomes part of the software lifecycle.


7. Resilience Patterns Belong in the Platform

Once the monolith becomes a collection of distributed services, failure modes multiply.

We standardized common resilience patterns:

Timeouts

Never allow a downstream dependency to block indefinitely.

Retries

Retry transient failures, but use bounded retries and appropriate backoff.

Dead-letter queues

Failed asynchronous messages should have somewhere explicit to go.

Idempotency

A retry should not accidentally create duplicate business operations.

Observability

Every workflow needs enough telemetry to answer:

What happened?
Where did it fail?
Why did it fail?
What was retried?
What is the current state?
Enter fullscreen mode Exit fullscreen mode

These shouldn't be reinvented independently by every team.

Where practical, they belong in shared platform capabilities and golden paths.


The Part We Almost Got Wrong

One of the biggest lessons from modernization is that distributed architecture introduces its own tax.

After extracting services, you now have:

  • More deployments
  • More network calls
  • More failure boundaries
  • More observability requirements
  • More IAM policies
  • More infrastructure components
  • More operational states

A badly designed microservices architecture can cost more—both financially and organizationally—than the monolith it replaced.

So the target shouldn't be:

"Maximum number of microservices."

It should be:

"The minimum number of independently scalable and independently deployable boundaries that make business sense."

That's a very different goal.


What We Measured

Cost was only one dimension.

For every migrated workload, we tracked a combination of financial, operational, and performance metrics.

Cost

  • Infrastructure cost per month
  • Cost per transaction
  • Cost per million requests
  • Cost per processed workload

Performance

  • p50 / p95 / p99 latency
  • Cold-start impact where relevant
  • Queue processing latency
  • Workflow duration

Reliability

  • Error rate
  • Retry rate
  • Dead-letter volume
  • Timeout rate
  • Failed workflow executions

Delivery

  • Deployment frequency
  • Deployment duration
  • Rollback frequency
  • Mean time to recovery

Looking at these metrics together prevents a common optimization mistake:

Saving money by making the system slower or less reliable isn't necessarily an improvement.


The Real Lesson

The most important lesson wasn't:

"Serverless is cheaper."

It was:

"Don't pay for infrastructure when your architecture doesn't need it."

The architecture evolved from:

Servers
   ↓
Processes
   ↓
Polling
   ↓
Work
Enter fullscreen mode Exit fullscreen mode

toward:

Events
   ↓
Managed orchestration
   ↓
On-demand compute
   ↓
Work
Enter fullscreen mode Exit fullscreen mode

The infrastructure became more closely aligned with actual business activity.

And that is where the savings came from.


A Practical Modernization Checklist

If you're starting a similar journey, I'd ask these questions before extracting the first service:

Workload

  • Is the workload stateless?
  • Is traffic spiky?
  • Does it have long idle periods?
  • Does it have a clear business boundary?
  • Can it scale independently?

Data

  • Does it have a clear data ownership model?
  • Can reads and writes be isolated?
  • What happens during partial failure?
  • Is the operation idempotent?

Reliability

  • What happens when a dependency times out?
  • What happens when a message is processed twice?
  • Where do permanently failed messages go?
  • How is recovery performed?

Operations

  • Can the service be deployed independently?
  • Can it be rolled back safely?
  • Can operators understand its state without reading application logs?
  • Is the infrastructure reproducible?

Economics

  • What is the current utilization?
  • What are we paying for when the system is idle?
  • What is the expected cost at current and peak volume?
  • What additional costs will serverless introduce?

Final Thought

Modernization isn't about making a monolith disappear.

It's about changing the economics, reliability, and delivery model of the system one boundary at a time.

Serverless was valuable because it allowed us to align compute more closely with actual work.

Event-driven architecture was valuable because it allowed the system to react instead of constantly polling.

Infrastructure as code was valuable because it made the new architecture repeatable.

And the 30–35% cost reduction was ultimately a consequence of those architectural decisions—not the reason to make them.

The best modernization strategy isn't "move to serverless."

It's:

Understand the workload. Find the boundary. Remove unnecessary always-on capacity. Make failure recoverable. Measure the result. Then repeat.


Top comments (0)