enekui
SMB tech  ·  7 min

Lift-and-shift Java monolith to ECS Fargate: 6-week migration case study

Industrial SMB with a 12-year-old Java monolith running on EC2 moved to containerised deployment on AWS ECS Fargate in six weeks, zero downtime, 23% lower monthly run cost. No application rewrite. This is what happened.


This case study is based on a real Enekui engagement completed in Q1 2025 for a European manufacturing client (€8M revenue, 4-person internal tech team). Names and specifics anonymised per NDA, technical approach and numbers are real.

The starting point

Client ran a monolithic Java 11 Spring Boot application on three EC2 t3.large instances behind an Application Load Balancer. PostgreSQL RDS, S3 for file storage, CloudWatch for logs. Stack deployed manually via SSH and shell scripts. No CI/CD. Deployments took 45 minutes, required after-hours coordination, occasional 2-3 minute downtime.

Monthly AWS bill: €1,840 (compute €920, RDS €680, data transfer €140, other €100).

Pain points: - Deployment friction: only two engineers comfortable deploying. - Scaling lag: manual intervention to add/remove instances, 8-10 minutes to spin up a new EC2. - Cost inefficiency: instances running 24/7 at 30-40% average CPU.

Why containers, why Fargate

Client didn't want a rewrite. Kubernetes was off the table (team size, operational overhead). ECS Fargate fit: - Lift-and-shift compatible: wrap the JAR in a Docker image, minimal code changes. - No instance management: Fargate abstracts the EC2 layer. - Better cost model for variable load: pay per vCPU-second, not per hour of idle capacity.

Alternative considered: Lambda with SnapStart. Rejected because the monolith has 90-second cold start (large dependency tree), and refactoring to smaller functions wasn't in scope.

Migration approach (6 weeks)

Week 1-2: Containerisation + local testing - Wrote a multi-stage Dockerfile: Maven build in one stage, slim JRE runtime in the final image (openjdk:11-jre-slim). - Externalised config: moved hardcoded DB credentials and S3 bucket names to environment variables (AWS Secrets Manager + Parameter Store). - Local Docker Compose setup mirroring prod (Postgres container, LocalStack for S3). Validated the app ran identically.

Week 3: ECS cluster + CI/CD - Terraform module for ECS Fargate cluster, task definition (2 vCPU, 4 GB RAM per task), ALB target group. - GitHub Actions pipeline: build Docker image → push to ECR → deploy new ECS task revision. - Deployment time dropped from 45 minutes to 6 minutes (rolling update, zero downtime).

Week 4: Parallel run + traffic split - Deployed ECS service in parallel with existing EC2 instances. - ALB weighted target groups: 10% traffic to Fargate, 90% to EC2. - Monitored error rates, latency (p50/p95/p99), database connection pool behaviour. No anomalies.

Week 5: Full cutover - Shifted 100% traffic to Fargate. - Kept EC2 instances running (stopped, not terminated) for 72 hours as rollback option. - No incidents. Decommissioned EC2 instances on day 4.

Week 6: Autoscaling + cost optimisation - Configured ECS Service Auto Scaling: target 60% CPU, min 2 tasks, max 6 tasks. - Enabled Fargate Spot for non-critical background jobs (70% cost saving on those tasks). - Tuned task size: reduced to 1 vCPU, 2 GB RAM after profiling (original 2 vCPU was over-provisioned).

Results

Cost: - New monthly bill: €1,410 (Fargate €520, RDS €680, data transfer €120, other €90). - 23% reduction vs EC2 baseline. Fargate compute is cheaper because we're not paying for idle capacity overnight (EU timezone, traffic drops 80% between 22:00-06:00).

Deployment: - Zero-downtime rolling updates, 6 minutes end-to-end. - Any engineer can deploy via GitHub merge to main. No SSH access needed.

Scaling: - Autoscaling kicks in at 60% CPU, new task running in 45 seconds (vs 8-10 minutes for EC2 Auto Scaling). - Handled a 3x traffic spike during a product launch (Black Friday equivalent) with zero manual intervention.

Downtime: - Zero unplanned downtime in 4 months post-migration. - One planned RDS maintenance window (15 minutes), application remained available (connection retry logic already in place).

Lessons learned

What went well: - Externalising config early (week 1) avoided last-minute surprises. Secrets Manager integration was trivial with ECS task IAM roles. - Parallel run (week 4) caught a subtle issue: the Docker image had a different timezone (UTC) than the EC2 instances (Europe/Madrid). Fixed by setting TZ env var in the task definition. - Terraform for everything (ECS, ALB, IAM, Secrets Manager) made the stack reproducible. We can spin up a staging environment in 8 minutes.

What we'd do differently: - Start with Fargate Spot from day one for non-prod environments. We enabled it in week 6; should've been week 3. - The initial task size (2 vCPU, 4 GB) was a guess. We over-provisioned by 50%. Run a profiling week earlier to right-size from the start. - CloudWatch Container Insights adds 15% to the Fargate bill. Useful for the first month, then we disabled it and relied on application-level metrics (Micrometer + Prometheus scraping). For an SMB, the cost/benefit didn't justify keeping it on.

Gotchas: - ECS task startup time is 30-45 seconds (image pull + health check). If your app has a 60-second cold start on top, you're looking at 90 seconds to serve traffic. This wasn't an issue for us (Spring Boot starts in 18 seconds), but it would've killed a Lambda migration. - Fargate networking: tasks get ephemeral IPs. If you have IP whitelisting (e.g. third-party API), you need a NAT Gateway (adds €35/month + data transfer costs). We already had one, but it's a hidden cost if you're migrating from EC2 with Elastic IPs.

What this means for your SMB

If you're running a monolith on EC2 and deployments feel like a chore, this is the lowest-risk path to modern infrastructure. You don't need to rewrite the app, you don't need Kubernetes, you don't need a 10-person platform team.

Key prerequisites: - Your app can externalise config (environment variables, not hardcoded). - You're comfortable with Docker basics (Dockerfile, image layers). - You have or can set up a CI/CD pipeline (GitHub Actions, GitLab CI, whatever).

If those three are true, you can replicate this in 4-6 weeks with a 2-person team (one backend engineer, one infrastructure engineer). The cost saving alone pays for the migration effort in 6-8 months.

How we'd apply it at Enekui

This is our standard SMB containerisation playbook. We use Terraform for all infrastructure (ECS cluster, task definitions, ALB, IAM roles, Secrets Manager). CI/CD via GitHub Actions or GitLab CI, depending on client preference. Monitoring with CloudWatch Logs + application-level metrics (we push to Prometheus or Datadog, depending on budget).

For a Java monolith like this, we'd add one extra step: integrate AWS X-Ray for distributed tracing. It's a 3-line code change (add the X-Ray SDK dependency, enable the daemon in the task definition) and gives you request-level visibility into database queries, S3 calls, external APIs. Costs €0.50 per million traces; on a 50k req/day app, that's €15/month. Worth it.

If your monolith is on .NET, Python, or Node, the approach is identical. The only difference is the Dockerfile base image and the health check endpoint.


Want help shipping this? We run a 2-week cloud diagnosis (€2,900) where we audit your current stack, model the Fargate migration (cost, timeline, risks), and deliver a Terraform proof-of-concept you can deploy to staging. If you decide to proceed, the diagnosis fee is credited toward the full migration engagement. Get in touch.

Fuentes