
A Series B startup. 23 microservices. 10,000 daily active users.
Their AWS bill: $47,000/month.
After consolidating to 4 services: $18,000/month.
That's $348,000 saved per year. Not from clever optimization. Not from better caching. Just from admitting they didn't need microservices.
The Lies We Tell Ourselves
Lie #1: "We Need Microservices to Scale"
Netflix handles 100,000 requests per second. You handle 100.
You're cosplaying as Netflix with 0.1% of their traffic. And paying enterprise prices for startup problems.
Here's the truth: A single m5.xlarge EC2 instance can handle 10,000 requests per second for typical CRUD operations. Cost: $140/month.
Your 23 microservices on t3.medium instances? $1,150/month. For 100 requests per second.
Lie #2: "Microservices Make Development Faster"
20 repositories. 20 CI/CD pipelines. 20 sets of dependencies.
One feature touches 5 services? That's 5 pull requests, 5 code reviews, 5 deployments, 5 places where something can break.
Your "2-day feature" just became a 2-week distributed systems debugging session.
Lie #3: "It's Industry Best Practice"
Best practice for whom? Amazon with 10,000 engineers? Or your 15-person startup?
Amazon created microservices because hundreds of teams were stepping on each other. You created microservices because a Medium article told you to.
Pull up your AWS Cost Explorer. I'll wait.
Now let's find the costs you're not seeing:
Data Transfer: The Silent Killer
Every API call between your services costs money. AWS charges $0.01/GB for cross-AZ transfer.
Let's do kindergarten math:
User request → API Gateway → Service A → Service B → Service C → Database
Each hop: ~10KB request + 50KB response = 60KB
1 million requests/day × 60KB × 5 hops = 300GB/day
Monthly damage: $900 just playing hot potato with your own data
Your monolith? Zero internal transfer costs. Zero.
The Load Balancer Tax
Each microservice needs an Application Load Balancer. AWS charges $0.025/hour + data processing fees.
20 services × $18/month = $360/month
Plus $0.008/GB processed. With 1TB/month per service: another $160.
Total: $520/month for the privilege of having multiple services.
One monolith needs one load balancer. $18/month. Done.
The Observability Black Hole
DataDog charges per host. CloudWatch charges per log group. New Relic charges per service.
Real numbers from a real startup:
DataDog: 20 hosts × $100/month = $2,000
CloudWatch Logs: 20 log groups × 50GB each = $500
CloudWatch Metrics: 200 custom metrics × $0.30 = $60
X-Ray tracing: 10 million traces × $0.000005 = $50
Total: $2,610/month to watch your 20 services do nothing special.
Same app as a monolith: $300/month.
Database Proliferation Disease
The microservices textbook says each service needs its own database.
20 services × RDS t3.small ($35/month) = $700/month
Plus backup storage, plus snapshots, plus read replicas for the important ones.
Real total: $1,400/month.
One RDS t3.xlarge handling everything: $280/month. With better performance.
The NAT Gateway Scam
Your private microservices need internet access. NAT Gateway: $45/month + $0.045/GB.
20 services downloading packages, pulling images, sending webhooks = 2TB/month.
NAT Gateway bill: $135/month.
Monolith in public subnet with security groups: $0/month.
When Microservices Actually Make Sense
Stop defending your architecture. Answer these honestly:
Do you have teams that CANNOT coordinate releases? Not "prefer not to." Not "it's annoying." CANNOT. If your teams can sit in one Zoom call, you don't need microservices.
Do different parts scale at 100x different rates? Not 2x. Not 10x. ONE HUNDRED TIMES. Your user service gets 1,000 req/sec while your PDF generator gets 10? That's 100x. You win. Extract it.
Do you NEED different programming languages? Not want. NEED. Machine learning in Python while everything else is in Java? Fine. Extract just that.
No to all three? You're burning $30,000/month for architectural purity.
The Smart Monolith Pattern
One codebase. Multiple modules. Clear boundaries. No network calls.
app/
├── billing/ # Domain logic
├── users/ # Domain logic
├── notifications/ # Domain logic
├── api/ # HTTP layer
└── shared/ # Common utilities
Each module has its own:
Domain models
Business logic
Database schema (same database, different schemas)
Tests
What it doesn't have:
Separate repository
Separate CI/CD pipeline
Separate monitoring
Separate deployment
Network calls to other modules
When you hit 10,000 requests per second (you won't), the boundaries are ready. Extract then. Not now.
The Extraction Threshold
Only extract a service when:
It's costing you customers The monolith literally cannot handle the load. Not "might not" in the future. Cannot. Today. Customers are leaving.
It's costing you engineers Teams are blocked weekly because of deployment conflicts. Not occasionally annoyed. Blocked. Weekly. Productivity is measurably dying.
It's costing you compliance PCI DSS requires payment processing isolation. HIPAA requires healthcare data separation. The auditor says "separate system" and means it.
Everything else is resume-driven development.
The Migration Math
Your current setup:
20 microservices
5-person engineering team
100 requests/second peak traffic
$15,000/month infrastructure
After consolidation to 3 services:
API monolith (95% of your logic)
Background jobs (the stuff that actually needs different scaling)
Notifications (the thing that spikes 10x during campaigns)
New costs:
Infrastructure: $4,000/month
Saved: $11,000/month
Annual savings: $132,000
That's two engineers' salaries. Or seed funding runway extension. Or actual features instead of fixing distributed system bugs.
Your Action Items Right Now
Run this query in CloudWatch Insights: Find out how many internal API calls you're making. Multiply by $0.01/GB. Cry.
Count your Load Balancers: Go to EC2 → Load Balancers. Count them. Multiply by $18. That's your ALB tax.
Check your DataDog bill: Settings → Usage → Hosts. How many are just the same app split up?
List services owned by the same team: If the same people own multiple services, why are they separate?
The truth nobody wants to hear: Your startup doesn't need microservices. You need a product that works, customers who pay, and money in the bank.
Stop playing distributed systems architect. Start shipping.
Your bank account will thank you.
⭐ Done For You Scripts + Step-By-Step Guides ⭐
I’ve created done-for-you scripts and detailed how-tos that will instantly reduce your cloud spend by migrating from unedded microservices to smart monolith, Here is what you will get below:
✅ Zero-AWS Access Calculator - Python script to calculate savings by simply inputting your EC2, RDS, ALB, and NAT Gateway counts
✅ 30% Efficiency Gain - Monoliths use 30% fewer resources due to eliminated network overhead and better resource utilization
✅ Real Cost Breakdown - See exactly how much you're spending on each microservice component vs consolidated infrastructure
✅ Instant ROI Analysis - Calculator shows monthly/annual savings and flags if you're burning an engineer's salary on complexity
✅ Strangler Pattern Migration - Safe consolidation using NGINX shadow traffic mirroring - no big-bang rewrites required
✅ Zero-Downtime Database Merge - Consolidate multiple small databases into one properly-sized instance using schema separation
✅ Smart Monolith Architecture - Domain-driven folder structure that's extraction-ready
✅ Docker Compose Setup - Local development environment that perfectly mirrors production
✅ Traffic Migration Strategy - Gradual weighted routing from microservices to monolith with instant rollback capability
✅ Environment Variable Switching - Toggle between monolith and microservice databases with a single config change
The Monolith Cost Calculator Script
This script requires zero AWS access - just edit the configuration at the top with your actual resource counts and see how much you're wasting on microservices complexity.
#!/usr/bin/env python3
"""
Monolith Cost Calculator - See what you'd save by consolidating
No AWS access needed - just count your resources and edit below
"""
# ============================================================
# EDIT THIS SECTION WITH YOUR ACTUAL INFRASTRUCTURE
# ============================================================
my_services = {
'ec2': {
't3.micro': 0, # How many t3.micro instances?
't3.small': 5, # How many t3.small instances?
't3.medium': 15, # How many t3.medium instances?
't3.large': 0, # How many t3.large instances?
't3.xlarge': 0, # How many t3.xlarge instances?
't3.2xlarge': 0, # How many t3.2xlarge instances?
'm5.large': 3, # How many m5.large instances?
'm5.xlarge': 0, # How many m5.xlarge instances?
'm5.2xlarge': 0, # How many m5.2xlarge instances?
},
'rds': {
'db.t3.micro': 0, # How many db.t3.micro databases?
'db.t3.small': 8, # How many db.t3.small databases?
'db.t3.medium': 3, # How many db.t3.medium databases?
'db.t3.large': 0, # How many db.t3.large databases?
'db.t4g.medium': 0, # How many db.t4g.medium databases?
'db.t4g.large': 0, # How many db.t4g.large databases?
'db.m5.large': 0, # How many db.m5.large databases?
'db.m5.xlarge': 0, # How many db.m5.xlarge databases?
},
'alb_count': 20, # How many Application Load Balancers?
'nat_gateway_count': 3, # How many NAT Gateways?
}
# ============================================================
# DON'T EDIT BELOW THIS LINE
# ============================================================
def calculate_current_costs(services):
"""Calculate current microservices infrastructure costs"""
# AWS pricing (us-east-1, on-demand, monthly)
ec2_monthly = {
't3.micro': 7.49,
't3.small': 15.18,
't3.medium': 30.37,
't3.large': 60.74,
't3.xlarge': 121.47,
't3.2xlarge': 242.94,
'm5.large': 69.12,
'm5.xlarge': 138.24,
'm5.2xlarge': 276.48,
}
rds_monthly = {
'db.t3.micro': 12.41,
'db.t3.small': 24.82,
'db.t3.medium': 49.64,
'db.t3.large': 99.28,
'db.t4g.medium': 45.26,
'db.t4g.large': 90.52,
'db.m5.large': 128.52,
'db.m5.xlarge': 257.04,
}
total = 0
breakdown = {}
# EC2 costs
for instance_type, count in services['ec2'].items():
if count > 0:
cost = ec2_monthly.get(instance_type, 50) * count
total += cost
breakdown[f'EC2 {instance_type}'] = {'count': count, 'monthly': cost}
# RDS costs
for instance_type, count in services['rds'].items():
if count > 0:
cost = rds_monthly.get(instance_type, 100) * count
total += cost
breakdown[f'RDS {instance_type}'] = {'count': count, 'monthly': cost}
# Load balancers
alb_cost = services.get('alb_count', 0) * 18
if alb_cost > 0:
total += alb_cost
breakdown['Load Balancers'] = {'count': services.get('alb_count', 0), 'monthly': alb_cost}
# NAT Gateways
nat_cost = services.get('nat_gateway_count', 0) * 32
if nat_cost > 0:
total += nat_cost
breakdown['NAT Gateways'] = {'count': services.get('nat_gateway_count', 0), 'monthly': nat_cost}
# Data transfer estimate (inter-service communication)
service_count = sum(services['ec2'].values())
if service_count > 1:
# Rough estimate: each service talks to 3 others, 100GB/month each
transfer_cost = min(service_count * 3, service_count * (service_count-1) / 2) * 1.0 # $1 per connection
total += transfer_cost
breakdown['Data Transfer (est)'] = {'count': f'{service_count} services', 'monthly': transfer_cost}
return total, breakdown
def calculate_monolith_costs(services):
"""Calculate equivalent monolith infrastructure costs"""
# Calculate total compute needed
total_vcpus = 0
vcpu_map = {
't3.micro': 2, 't3.small': 2, 't3.medium': 2,
't3.large': 2, 't3.xlarge': 4, 't3.2xlarge': 8,
'm5.large': 2, 'm5.xlarge': 4, 'm5.2xlarge': 8
}
for instance_type, count in services['ec2'].items():
total_vcpus += vcpu_map.get(instance_type, 2) * count
# Monolith is 30% more efficient (no network overhead, better resource usage)
needed_vcpus = int(total_vcpus * 0.7)
# Size the monolith
monolith = {}
if needed_vcpus <= 4:
monolith['ec2'] = {'t3.xlarge': 1}
elif needed_vcpus <= 8:
monolith['ec2'] = {'t3.2xlarge': 1}
elif needed_vcpus <= 16:
monolith['ec2'] = {'m5.2xlarge': 2}
else:
instance_count = max(3, needed_vcpus // 8) # At least 3 for HA
monolith['ec2'] = {'m5.2xlarge': instance_count}
# Database consolidation
total_db_count = sum(services['rds'].values())
if total_db_count == 0:
monolith['rds'] = {}
elif total_db_count <= 3:
monolith['rds'] = {'db.t4g.large': 1}
elif total_db_count <= 10:
monolith['rds'] = {'db.m5.xlarge': 1}
else:
monolith['rds'] = {'db.m5.xlarge': 2}
# Monolith needs minimal infrastructure
monolith['alb_count'] = 1 # Just one ALB
monolith['nat_gateway_count'] = 0 # Public subnet with security groups
return calculate_current_costs(monolith)
def generate_report(services):
"""Generate the cost comparison report"""
current_cost, current_breakdown = calculate_current_costs(services)
monolith_cost, monolith_breakdown = calculate_monolith_costs(services)
monthly_savings = current_cost - monolith_cost
print("\n" + "="*60)
print("💰 MICROSERVICES VS MONOLITH COST ANALYSIS")
print("="*60)
print("\n📊 CURRENT MICROSERVICES SETUP:")
for item, details in current_breakdown.items():
print(f" {item}: {details['count']} × ${details['monthly']/details['count'] if details['count'] else 0:.2f} = ${details['monthly']:.2f}/mo")
print(f"\n TOTAL: ${current_cost:,.2f}/month (${current_cost*12:,.2f}/year)")
print("\n🎯 EQUIVALENT MONOLITH SETUP:")
for item, details in monolith_breakdown.items():
if details['monthly'] > 0:
print(f" {item}: {details['count']} × ${details['monthly']/details['count'] if details['count'] else 0:.2f} = ${details['monthly']:.2f}/mo")
print(f"\n TOTAL: ${monolith_cost:,.2f}/month (${monolith_cost*12:,.2f}/year)")
print("\n" + "="*60)
print(f"💸 MONTHLY SAVINGS: ${monthly_savings:,.2f}")
print(f"💰 ANNUAL SAVINGS: ${monthly_savings * 12:,.2f}")
print(f"📉 COST REDUCTION: {(monthly_savings/current_cost)*100:.1f}%")
print("="*60)
if monthly_savings > 10000:
print("\n🚨 HOLY SH*T: You're burning a senior engineer's salary on complexity!")
elif monthly_savings > 5000:
print("\n⚠️ WARNING: That's a junior engineer's salary in savings!")
elif monthly_savings > 1000:
print("\n💡 That's real money. Time to consolidate.")
else:
print("\n✅ Your architecture is reasonably cost-efficient.")
return monthly_savings
if __name__ == "__main__":
savings = generate_report(my_services)
if savings > 1000:
print("\n📝 NEXT STEPS:")
print("1. Start with services owned by the same team")
print("2. Consolidate read-heavy services first")
print("3. Keep services with different scaling patterns separate")
print("4. Use the strangler pattern - no big bang
rewrites")
Exmple output from calculator to tell you savings:
$ python monolith_calculator.py
============================================================
💰 MICROSERVICES VS MONOLITH COST ANALYSIS
============================================================
📊 CURRENT MICROSERVICES SETUP:
EC2 t3.small: 5 × $15.18 = $75.90/mo
EC2 t3.medium: 15 × $30.37 = $455.55/mo
EC2 m5.large: 3 × $69.12 = $207.36/mo
RDS db.t3.small: 8 × $24.82 = $198.56/mo
RDS db.t3.medium: 3 × $49.64 = $148.92/mo
Load Balancers: 20 × $18.00 = $360.00/mo
NAT Gateways: 3 × $32.00 = $96.00/mo
Data Transfer (est): 23 services = $23.00/mo
TOTAL: $1,565.29/month ($18,783.48/year)
🎯 EQUIVALENT MONOLITH SETUP:
EC2 m5.2xlarge: 3 × $276.48 = $829.44/mo
RDS db.m5.xlarge: 1 × $257.04 = $257.04/mo
Load Balancers: 1 × $18.00 = $18.00/mo
TOTAL: $1,104.48/month ($13,253.76/year)
============================================================
💸 MONTHLY SAVINGS: $460.81
💰 ANNUAL SAVINGS: $5,529.72
📉 COST REDUCTION: 29.4%
============================================================
💡 That's real money. Time to consolidate.
📝 NEXT STEPS:
1. Start with services owned by the same team
2. Consolidate read-heavy services first
3. Keep services with different scaling patterns separate
4. Use the strangler pattern - no big bang rewrites
The Strangler Consolidation Pattern
Instead of a risky big-bang migration, gradually strangle your microservices by consolidating them behind their existing API contracts.
Phase 1: Shadow Mode Deploy your monolith alongside existing services. Use NGINX to duplicate traffic with the mirror feature
# nginx.conf snippet for shadow testing
upstream microservices {
server user-service.internal:8080;
server order-service.internal:8080;
}
upstream monolith {
server monolith.internal:8080;
}
location /api/ {
proxy_pass http://microservices;
# Shadow traffic to monolith (doesn't affect response)
mirror /shadow;
}
location /shadow {
internal;
proxy_pass http://monolith$request_uri;
}
Key Points:
Never break existing connections
Monitor error rates at each step
Keep rollback ready (just update weights back)
Database Consolidation Without Downtime
Merge your 20 tiny databases into one properly-sized instance without losing data or causing downtime.
Step 1: Create Consolidated Instance
bash
# Create new RDS instance with multiple schemas
aws rds create-db-instance \
--db-instance-identifier monolith-db \
--db-instance-class db.m5.xlarge \
--engine postgres \
--master-username admin \
--master-user-password $PASSWORD \
--allocated-storage 500
Use this environment variable to easily switch between monolith and microservice while you migrate
# Zero-downtime connection switch using environment variables
# Deploy this to your services gradually
import os
def get_db_connection():
if os.getenv('USE_MONOLITH_DB', 'false').lower() == 'true':
return {
'host': 'monolith-db.internal',
'port': 5432,
'database': 'monolith',
'schema': os.getenv('SERVICE_NAME') # Each keeps its schema
}
else:
# Original microservice DB
return {
'host': f"{os.getenv('SERVICE_NAME')}-db.internal",
'port': 5432,
'database': os.getenv('SERVICE_NAME')
}
The Smart Monolith Architecture
Structure your monolith to be ready for extraction (but you probably won't need to).
Project Structure:
monolith/
├── docker-compose.yml # Local dev matching prod
├── Dockerfile
├── src/
│ ├── api/ # HTTP layer
│ │ ├── users.py
│ │ ├── orders.py
│ │ └── router.py
│ ├── domains/ # Business logic
│ │ ├── users/
│ │ │ ├── service.py
│ │ │ ├── repository.py
│ │ │ └── models.py
│ │ ├── orders/
│ │ └── shared/
│ └── infrastructure/ # Cross-cutting concerns
│ ├── database.py
│ └── messaging.py
Docker Compose for Local = Production:
# docker-compose.yml
version: '3.8'
services:
monolith:
build: .
ports:
- "8080:8080"
environment:
- DB_HOST=postgres
- REDIS_HOST=redis
depends_on:
- postgres
- redis
postgres:
image: postgres:14
environment:
POSTGRES_DB: monolith
volumes:
- ./init-schemas.sql:/docker-entrypoint-initdb.d/init.sql
redis:
image: redis:7
With this info you should be good to go