How We Enabled Horizontal Scaling for a Monolithic App on AWS Cloud
Scaling a monolithic application horizontally on AWS Cloud requires a strategic approach to architecture and infrastructure. Here's how we accomplished it.
The Challenge
Our client had a monolithic application that was hitting performance limits. The application was deployed on a single EC2 instance, and vertical scaling (increasing instance size) was becoming too expensive and had physical limits.
Solution Architecture
1. Application Layer Separation
We separated concerns within the monolithic application:
- Stateless Application Servers: Moved session data to external storage
- Database Layer: Migrated to Amazon RDS with read replicas
- Static Assets: Moved to Amazon S3 with CloudFront CDN
- Cache Layer: Implemented Amazon ElastiCache (Redis)
2. Load Balancing
Implemented Application Load Balancer (ALB):
# ALB Configuration LoadBalancer: Type: application Scheme: internet-facing SecurityGroups: - sg-xxxxxxxxx Subnets: - subnet-xxxxxxxx - subnet-yyyyyyyy HealthCheck: Path: /health Interval: 30 Timeout: 5 HealthyThreshold: 2 UnhealthyThreshold: 3
3. Auto Scaling Groups
Configured EC2 Auto Scaling:
AutoScalingGroup: MinSize: 2 MaxSize: 10 DesiredCapacity: 4 HealthCheckType: ELB HealthCheckGracePeriod: 300 TargetGroups: - arn:aws:elasticloadbalancing:... ScalingPolicies: - Type: TargetTrackingScaling TargetValue: 70 PredefinedMetricSpecification: PredefinedMetricType: ASGAverageCPUUtilization
Implementation Steps
Step 1: Session State Externalization
Moved session storage from in-memory to Redis:
// Before: In-memory sessions const session = require('express-session'); app.use(session({ store: new MemoryStore(), secret: 'secret' })); // After: Redis sessions const RedisStore = require('connect-redis')(session); const redisClient = require('redis').createClient({ host: process.env.REDIS_HOST, port: 6379 }); app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));
Step 2: Database Optimization
- Created read replicas for read-heavy operations
- Implemented connection pooling
- Optimized queries and added indexes
- Set up database monitoring with CloudWatch
Step 3: Static Asset Migration
// Upload to S3 const AWS = require('aws-sdk'); const s3 = new AWS.S3(); // Serve from CloudFront const staticUrl = 'https://d123456789.cloudfront.net';
Step 4: Health Check Endpoint
app.get('/health', async (req, res) => { try { // Check database connection await db.query('SELECT 1'); // Check Redis connection await redis.ping(); res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); } catch (error) { res.status(503).json({ status: 'unhealthy', error: error.message }); } });
Results
After implementation:
- 99.99% Uptime: No single point of failure
- 5x Traffic Capacity: Handled peak loads smoothly
- 40% Cost Reduction: Efficient resource utilization
- Sub-second Response Times: Improved user experience
- Automatic Recovery: Self-healing infrastructure
Best Practices We Followed
- Stateless Design: No local state on application servers
- Idempotent Operations: Safe to retry failed requests
- Graceful Degradation: System remains functional during partial failures
- Comprehensive Monitoring: CloudWatch metrics and alarms
- Blue-Green Deployments: Zero-downtime releases
- Infrastructure as Code: Terraform for reproducible environments
Monitoring Setup
CloudWatch Alarms: - Name: HighCPUUtilization Metric: CPUUtilization Threshold: 80 Action: SNS notification + scale up - Name: HealthyHostCount Metric: HealthyHostCount Threshold: 1 Action: SNS alert - Name: ResponseTime Metric: TargetResponseTime Threshold: 1000ms Action: SNS notification
Lessons Learned
- Start Small: We scaled gradually, not all at once
- Test Thoroughly: Load testing was crucial before production
- Document Everything: Clear runbooks for operations team
- Monitor Proactively: Set up alerts before issues occur
- Plan for Failure: Design for resilience from day one
Conclusion
Horizontal scaling transformed our monolithic application from a bottleneck to a scalable, resilient system. The key was proper architecture, external state management, and AWS best practices.
Key Takeaways
- External session storage is essential for horizontal scaling
- Load balancers distribute traffic effectively
- Auto Scaling Groups provide elasticity and fault tolerance
- Monitoring and health checks ensure system reliability
- Infrastructure as Code makes scaling reproducible

