Back to Blog
HOW TO?

How We Enabled Horizontal Scaling for a Monolithic App on AWS Cloud

8 min read

DevOps Team

Cloud Solutions Architect

How We Enabled Horizontal Scaling for a Monolithic App on AWS Cloud

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

  1. Stateless Design: No local state on application servers
  2. Idempotent Operations: Safe to retry failed requests
  3. Graceful Degradation: System remains functional during partial failures
  4. Comprehensive Monitoring: CloudWatch metrics and alarms
  5. Blue-Green Deployments: Zero-downtime releases
  6. 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

  1. Start Small: We scaled gradually, not all at once
  2. Test Thoroughly: Load testing was crucial before production
  3. Document Everything: Clear runbooks for operations team
  4. Monitor Proactively: Set up alerts before issues occur
  5. 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

Related Posts