
Ask Claude Code or Cursor to set up a multi-service TypeScript backend, and you'll get roughly the same stack every time: Express for the HTTP layer, a Dockerfile per service, a docker-compose.yml to wire them together locally, and Terraform modules for the AWS resources in production. It's the pattern with the most surface area in training data, so agents reproduce it by default.
The stack works. It's also a lot of configuration for what amounts to "run my TypeScript services on AWS with a database and a message queue."
Here's what a typical two-service backend looks like with the Docker Compose + Terraform approach. An orders service and a notifications service, with a PostgreSQL database and a message queue.
The application code (the part that does the work):
orders/src/index.ts Express app, ~80 lines orders/src/routes.ts API endpoints, ~60 lines orders/src/db.ts database connection pool, ~25 lines orders/src/queue.ts BullMQ producer setup, ~20 lines notifications/src/index.ts Express app, ~40 lines notifications/src/worker.ts BullMQ consumer, ~30 lines
About 250 lines of application code across two services.
The configuration layer (the part that wires and deploys it):
orders/Dockerfile 25 lines notifications/Dockerfile 25 lines docker-compose.yml 45 lines terraform/main.tf 30 lines terraform/vpc.tf 60 lines terraform/rds.tf 45 lines terraform/elasticache.tf 35 lines terraform/ecs.tf 80 lines terraform/alb.tf 50 lines terraform/iam.tf 40 lines terraform/ecr.tf 15 lines terraform/outputs.tf 10 lines terraform/variables.tf 30 lines .github/workflows/deploy.yml 60 lines
About 550 lines of configuration. More than twice the application code. For two services.
And the configuration isn't static. Every time you add a service, you add a Dockerfile, an ECS task definition, a target group, IAM roles, and entries in the docker-compose and CI/CD pipeline. Every time you add a database, you add an RDS module with its security group and subnet group.
Here's the same two-service backend built with Encore.ts:
// orders/orders.ts
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { Topic } from "encore.dev/pubsub";
// Declares a PostgreSQL database. Encore provisions RDS on AWS.
const db = new SQLDatabase("orders", { migrations: "./migrations" });
// Declares a Pub/Sub topic. Encore provisions SNS + SQS per subscriber.
export const orderCreated = new Topic<OrderEvent>("order-created", {
deliveryGuarantee: "at-least-once",
});
// Defines a type-safe API endpoint. Encore handles routing and validation.
export const create = api(
{ expose: true, method: "POST", path: "/orders" },
async (req: CreateOrderRequest): Promise<Order> => {
const order = await db.queryRow<Order>`
INSERT INTO orders (customer_id, total)
VALUES (${req.customerId}, ${req.total})
RETURNING id, customer_id as "customerId", total`;
await orderCreated.publish({
orderId: order!.id,
customerId: req.customerId,
total: req.total,
});
return order!;
}
);
// notifications/notifications.ts
import { Subscription } from "encore.dev/pubsub";
import { orderCreated } from "../orders/orders";
// Subscribes to the order-created topic. Encore handles delivery and retries.
const _ = new Subscription(orderCreated, "send-notification", {
handler: async (event) => {
console.log(`Order ${event.orderId}: sending notification`);
},
});
About 40 lines total, with no Dockerfiles, docker-compose.yml, Terraform directory, or CI/CD pipeline config.
| Docker Compose + Terraform stack | Encore |
|---|---|
orders/Dockerfile (25 lines) | Not needed. Encore builds container images |
notifications/Dockerfile (25 lines) | Not needed |
docker-compose.yml (45 lines) | Not needed. encore run handles local dev |
terraform/vpc.tf (60 lines) | Not needed. VPC provisioned automatically |
terraform/rds.tf (45 lines) | new SQLDatabase("orders", ...), 1 line |
terraform/elasticache.tf (35 lines) | Not needed. Encore uses SNS+SQS, not Redis |
terraform/ecs.tf (80 lines) | Not needed. Services derived from code |
terraform/alb.tf (50 lines) | Not needed. Load balancer provisioned automatically |
terraform/iam.tf (40 lines) | Not needed. Least-privilege IAM generated from code |
terraform/ecr.tf (15 lines) | Not needed |
terraform/variables.tf (30 lines) | Not needed |
.github/workflows/deploy.yml (60 lines) | Not needed. Deploy on git push |
| ~550 lines of config | 0 lines of config |
The AWS resources are the same: Fargate, RDS, SNS+SQS, a load balancer, IAM roles. The difference is who wrote the configuration.
The docker-compose.yml is easy to underestimate. It's only 45 lines, but it's the thing you interact with every day. docker-compose up takes 30 seconds. Rebuilding after a change takes longer. The PostgreSQL data volume occasionally gets corrupted. Redis needs to be running even though you're only working on one service.
encore run starts both services with a real PostgreSQL instance (provisioned via Docker) and pub/sub with production-equivalent delivery semantics. Hot reload on file changes, and a local dashboard at localhost:9400 with distributed traces, an API explorer, and a database browser.

The difference between a 30-second startup with a container rebuild cycle and a 2-second startup with hot reload shapes how you work every day.
When an agent works in a project with Docker Compose and Terraform, every feature that touches infrastructure requires changes in multiple places: the application code, the docker-compose, the Terraform, and the CI/CD pipeline. The agent can generate all of it, but each piece has to be reviewed separately, and they can drift from each other.
When the agent works in an Encore project, a new feature that needs a database is const db = new SQLDatabase(...). A new pub/sub topic is new Topic(...). A new service is a new directory. The agent spends its time on business logic instead of configuration.
If you have an existing Docker Compose + Terraform stack, you don't have to rewrite everything at once. Encore supports incremental adoption: start with one service, deploy it alongside your existing infrastructure, and migrate service by service. Each service you move removes a Dockerfile, a Terraform module, and the associated maintenance surface.
You can also generate Docker images with encore build docker for any service, so self-hosting remains an option.