Oct 23, 20268 min read

Python Microservices: Architecture and Infrastructure

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.

When microservices make sense in Python

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.

Architecture at a glance

ConcernCommon Python approachWith Encore
Service boundaryOne FastAPI app per repository or directoryOne FastAPI app per directory with an encore.service.json
Database per serviceA Postgres container locally, an RDS or Cloud SQL instance per service in productionpostgres.Database declared in the service, provisioned locally and in your cloud
Synchronous callsHTTP with httpx, URLs from environment variablesHTTP with httpx, URLs from base_url("service")
Asynchronous eventsCelery, RabbitMQ, Kafka or a cloud queue you runpubsub.Topic and pubsub.Subscription declared in Python
Local developmentDocker Compose for databases, broker and servicesencore run starts the services, databases and Pub/Sub
DeploymentDockerfiles, a registry and ECS, Kubernetes or Cloud Run configured per serviceECS Fargate or EKS on AWS, Cloud Run or GKE on GCP, in your own account

Adding Encore to Python microservices

Encore

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.

Application code passes through Encore and infrastructure guardrails to resources provisioned in your own AWS or GCP account.
Encore provisions the infrastructure your application needs in your own AWS or GCP account.

Project layout

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 } ] }

The users service and its database

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.

Synchronous calls between services

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.

Asynchronous events with Pub/Sub

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.

Running it locally and deploying

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.

Good to know

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.

Try Encore →

Building the same system without a platform

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.

Common patterns

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.

When to split a service

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.

Frequently asked questions

Is Python good for microservices?

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.

Which Python framework is best for microservices?

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.

How do Python microservices communicate?

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.

Should each Python microservice have its own database?

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.

How do you deploy Python microservices?

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.

Can Python microservices run alongside services in other languages?

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.

How do I add a database and Pub/Sub to Python microservices?

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.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders