How to split a Python backend into services, how they talk to each other, and what it takes to run them locally and on AWS or GCP.
A Python microservices system is a set of small backends, usually FastAPI apps, that each own one part of the domain and their own data and talk to each other over HTTP or through events. Splitting the code is usually straightforward, and most of the work is in what surrounds it, since each service needs its own Postgres database, a broker for events, a way to find the other services and its own deployment, so a three-service system on a laptop turns into a long Docker Compose file while production needs the same pieces described again in Terraform or Kubernetes manifests. This guide goes through the architecture decisions first and then builds a small users and orders system in Python with the infrastructure declared in code. It is the Python companion to the Go microservices guide.
Most Python backends should start as one service, or a modular monolith with clear packages, and split later. Splitting pays off when parts of the system need to scale differently, such as an API that is light on CPU next to a worker that runs model inference, when separate teams keep blocking each other's releases, or when one part needs a different runtime or dependency set. The monolith vs microservices guide goes deeper on the tradeoff.
Encore combines each service's existing FastAPI app with the infrastructure it uses, as an SDK added to the app rather than a framework that replaces it, so a service declares its Postgres database and Pub/Sub topics as module-level Python variables, encore run starts every service with its own local database (Postgres runs in Docker), and a deploy provisions the same services, databases and topics in your own AWS or GCP account. HTTP routing stays in FastAPI, because the SDK only handles infrastructure and service discovery, and it works the same way with whichever framework a service uses, such as Django.

An Encore app has one encore.app.json at the root, and every directory with an encore.service.json is a service that runs in its own process. The example below has a public users service and an internal orders service:
shop/ ├── encore.app.json ├── users/ │ ├── encore.service.json │ ├── main.py │ └── migrations/ └── orders/ ├── encore.service.json ├── main.py └── migrations/
The service file names the service and its language, and the expose flag on an API entry decides whether the public gateway routes to it, so leaving it out keeps the service reachable only from other services:
{
"name": "users",
"lang": "python",
"api": [
{ "type": "http", "prefix": "/", "name": "users", "expose": true }
]
}
Each service declares its own database, and Encore applies the migrations in the given directory before the service starts:
# users/main.py
from encore import pubsub
from encore.storage import postgres
from fastapi import FastAPI
from pydantic import BaseModel
db = postgres.Database("users", migrations="./migrations")
signups = pubsub.Topic("signups")
app = FastAPI()
class Signup(BaseModel):
email: str
@app.post("/users", status_code=201)
async def create_user(req: Signup) -> dict:
row = await db.query(
"INSERT INTO users (email) VALUES ($1) RETURNING id", req.email
).one()
await signups.publish({"user_id": row["id"], "email": req.email})
return {"id": row["id"]}
@app.get("/users/{user_id}")
async def get_user(user_id: int) -> dict:
return await db.query("SELECT * FROM users WHERE id = $1", user_id).one()
The orders service declares a separate orders database, so the two schemas can change independently and neither service can read the other's tables.
The SDK's base_url returns the configured URL of another service, and the call itself goes through whatever HTTP client you already use, here httpx:
# orders/main.py
import httpx
from encore.services import base_url
from encore.storage import postgres
from fastapi import FastAPI
db = postgres.Database("orders", migrations="./migrations")
app = FastAPI()
@app.post("/orders/{user_id}", status_code=201)
async def place_order(user_id: int) -> dict:
async with httpx.AsyncClient() as client:
resp = await client.get(f"{base_url('users')}/users/{user_id}")
resp.raise_for_status()
row = await db.query(
"INSERT INTO orders (user_id) VALUES ($1) RETURNING id", user_id
).one()
return {"id": row["id"]}
Encore reads the dependency on users from the code, which is why the service name has to be a string literal passed directly to base_url, and the lookup reads runtime configuration without a network request.
When the caller does not need an answer, an event keeps the services decoupled, and in this example the users service already publishes to signups, and any service can subscribe with a handler that receives a typed message:
from encore import pubsub
from pydantic import BaseModel
class SignupEvent(BaseModel):
user_id: int
email: str
signups = pubsub.Topic("signups")
async def send_welcome(msg: pubsub.Message[SignupEvent]) -> None:
await send_email(msg.data.email)
welcome = pubsub.Subscription[SignupEvent](
signups,
"send-welcome",
handler=send_welcome,
max_retries=3,
)
Payloads are decoded into the Pydantic model before the handler runs, and a handler that raises is retried, which is the same role a Celery worker plays in many Python stacks.
encore run starts both services side by side, each with its own Postgres database and the Pub/Sub topic between them, so the whole system is testable on a laptop without a Compose file. A deploy provisions each service on ECS Fargate or EKS in AWS or Cloud Run or GKE in GCP, with RDS or Cloud SQL databases, SNS/SQS or Google Pub/Sub, and an IAM role per service that only grants access to that service's resources. Services written in TypeScript, Go or Rust can live in the same app and call the Python services the same way.
When an AI coding agent adds a new service, it is one more directory with its own database declaration, which it can run next to the others locally without writing Terraform for it.
Encore uses its own query and exec methods with $1 style parameters for database access, so code built on an ORM session needs adapting, and existing RDS databases can be imported rather than recreated. Each service can use whichever framework suits it, which FastAPI vs Django vs Flask and the best Python backend frameworks compare, and deploying FastAPI to AWS walks through a single service from local run to production. Python support is in beta, and you can sign up for access.
The pieces are the same when you assemble them yourself, and the work moves into configuration. Locally, a Compose file runs a Postgres container per service, a broker such as RabbitMQ and each FastAPI app, with service URLs passed in as environment variables:
services:
users-db:
image: postgres:17
environment:
POSTGRES_PASSWORD: dev
orders-db:
image: postgres:17
environment:
POSTGRES_PASSWORD: dev
rabbitmq:
image: rabbitmq:4-management
users:
build: ./users
environment:
DATABASE_URL: postgresql://postgres:dev@users-db/postgres
orders:
build: ./orders
environment:
DATABASE_URL: postgresql://postgres:dev@orders-db/postgres
USERS_URL: http://users:8000
In production each of those lines becomes cloud resources, which on AWS means an ECR repository, an ECS service and task role per service, an RDS instance per database, Amazon MQ or SQS for messaging and service discovery through ECS Service Connect, usually written in Terraform so it can be reviewed and repeated. That approach gives you full control and works with any library, but it means keeping the Compose file, the Terraform and the code in agreement as services are added.
API gateway. Only some services should be reachable from the internet, and with Encore that is the expose setting in encore.service.json, and in a hand-built setup it is a load balancer or API Gateway in front of the public services. The API gateway guide covers the options.
Saga. A workflow that spans services, such as reserving stock and then charging a card, cannot use one database transaction, so each step publishes an event and failures trigger compensating steps in the services that already acted.
Shared code. Shared Pydantic models for events are tempting, but they couple release cycles, so many teams copy the small event schemas into each service or publish them as a versioned package.
Split a service out when you can name the concrete reason, such as a different scaling profile, a team that needs to ship on its own schedule or a dependency set that conflicts with the rest. Splitting by technical layer, such as a separate service for database access, adds network calls without giving you the independence that is the reason to split in the first place.
Yes. Frameworks like FastAPI give each service typed request handling and async I/O, and Python's ecosystem covers most of what services need. The tradeoffs are higher memory use per process than Go and the need to run several worker processes for CPU-bound work, which matters more as the number of services grows.
FastAPI is the most common choice for new services because it is async, validates requests with Pydantic and generates OpenAPI docs. Flask and Django also work, Django mostly for services with a lot of admin and ORM needs. The framework matters less than how you handle communication, data ownership and infrastructure between services.
Synchronously over HTTP or gRPC when the caller needs an answer right away, and asynchronously through a message broker or Pub/Sub topic when it does not. Events keep services independent of each other's uptime at the cost of eventual consistency.
Yes. A database per service keeps schemas independent, so one team can migrate its tables without breaking another service. When a service needs another's data, it calls that service's API or keeps its own copy updated from events.
Each service is built into a container image and run on ECS, Kubernetes or Cloud Run, with networking, service discovery, secrets and IAM configured per service. Encore declares the databases and topics in Python and provisions the services and infrastructure in your own AWS or GCP account.
Yes. Services communicate over the network, so a Python service can call a Go or TypeScript service as long as they agree on the API. Encore runs services written in Python, TypeScript, Go and Rust in the same app and wires up the calls between them.
Add Encore's SDK to each service's existing app, give the service an encore.service.json file, and declare postgres.Database, pubsub.Topic and pubsub.Subscription as module-level variables in the service that owns them. encore run starts every service with its own local database and the topics between them, and a deploy provisions the same resources in your own AWS or GCP account.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.