Background
A mid-size e-commerce retailer had grown its technology platform incrementally over eight years. The core systems — order management, fulfilment, and inventory — were Java applications running on on-premise hardware in a co-location facility. The platform worked, but the operational model was expensive and fragile at the edges: seasonal traffic peaks required manually provisioning additional hardware 4–6 weeks in advance, and that hardware sat largely idle for nine months of the year.
The platform was also slowing down product delivery. Deployments required a scheduled 4-hour maintenance window because services weren't designed to deploy independently — a shared database schema and synchronous inter-service HTTP calls meant that rolling deployments caused inconsistency errors.
The Constraint
The seasonal peak was 10 months away when the engagement started. The retailer's previous peak had resulted in partial outages under load — the on-premise platform had been manually provisioned at what the team estimated was 8x normal traffic, but the actual peak reached approximately 9.5x. Several critical services had degraded under load, resulting in order processing delays and customer support volume that cost more operationally than the additional infrastructure would have.
The board had set a clear requirement: be on cloud infrastructure and capable of handling at least 12x normal traffic before the next peak. The 10-month timeline was fixed.
Migration Strategy: Lift-and-Modernize
A pure lift-and-shift to AWS EC2 — moving the applications as-is — would have met the scalability requirement through EC2 Auto Scaling Groups, but wouldn't have solved the deployment problem. The 4-hour maintenance windows were caused by architectural coupling, not infrastructure. We recommended a lift-and-modernize approach: containerize the services and introduce an event-driven messaging layer to replace the synchronous inter-service calls.
The synchronous HTTP calls between services were the root cause of the deployment coupling. When order management was being deployed, fulfilment would receive errors on in-flight calls, causing transaction inconsistencies that required the maintenance window to drain and verify. Replacing those calls with asynchronous messaging via Amazon SQS meant services could be deployed independently — a deployment of the fulfilment service wouldn't affect order management in flight.
AWS ECS was chosen over EKS based on operational simplicity — the retailer's infrastructure team was small and didn't have Kubernetes experience. ECS Fargate with auto-scaling policies was sufficient for the use case and significantly reduced operational overhead.
Migration Execution
The migration was executed service by service across 8 months. For each service, the process was: containerize the application, write Terraform configuration for the ECS task definition and supporting infrastructure, implement the SQS/SNS integration for the service's outbound events, deploy to a staging environment in AWS, run load testing to validate auto-scaling behaviour, then cut production traffic over.
Database migration was separated from application migration. RDS PostgreSQL instances in AWS were set up with replication from the on-premise databases, maintaining consistency during the parallel-running period. When each application service was migrated, its database connection was switched to RDS. The on-premise databases continued as replicas until all services had been migrated, at which point replication was stopped and the on-premise instances decommissioned.
Terraform code covered all infrastructure — ECS clusters, task definitions, auto-scaling policies, ALBs, RDS instances, ElastiCache clusters, SQS queues, IAM roles, and VPC configuration. Every infrastructure change went through pull request review and was applied via a CI/CD pipeline, eliminating the manual AWS console operations the team had initially used.
Load Testing
Before the peak season, we ran a full-scale load test simulating the target traffic profile: a gradual ramp from normal load to 12x normal over 90 minutes, held for 30 minutes, then a rapid spike to 14x to test the auto-scaling response time. Every service held at 12x without any degradation. The auto-scaling response time — from the point a new task was needed to the point it was serving traffic — averaged 47 seconds, which was within the acceptable threshold for the retailer's traffic patterns.
The Peak Season
The actual peak reached approximately 11x normal traffic — lower than the load test ceiling. Auto-scaling handled the load without any manual intervention. The team's on-call engineer monitored the dashboards but took no actions. There were no order processing failures attributable to the platform. The fulfilment team reported that operations during the peak were indistinguishable from a normal trading day from their perspective.
Results
On an annualised basis, the infrastructure costs reduced by 34% compared to the on-premise estate — the combination of right-sized compute, auto-scaling (which eliminated over-provisioned idle capacity), and Reserved Instance discounts for the baseline compute was materially cheaper than co-location costs plus hardware refresh cycles. The 4-hour maintenance windows were eliminated: deployments became rolling with zero downtime, and the team shifted to deploying during business hours rather than scheduling Saturday evening windows.
Lessons
The database migration was the most time-consuming element and took longer than the initial plan. The on-premise Oracle-to-PostgreSQL migration for two services required more data transformation work than anticipated — some stored procedures embedded business logic that needed to be extracted and rewritten as application code before migration. This should have been scoped more carefully during discovery. A clearer database assessment at the start of the engagement would have surfaced these issues three months earlier.