DEV Community

Cover image for Microservices Python Example: Real Patterns for Production
Ayush Kumar
Ayush Kumar

Posted on Originally published at logiclooptech.dev

Microservices Python Example: Real Patterns for Production

Microservices python example is what you need when splitting a monolith into independently deployable services. I’ve run this in production for over two years, and it’s taught me where the theory meets the mess.

What does a clean project structure look like for Python microservices?

I start with a repo per service. Each owns its code, dependencies, and data model. No shared databases. No imports across service boundaries. Inside each service:

/app
  /app
    main.py          # FastAPI entrypoint
    routers/         # API versioned routers
    services/        # Business logic
    models/          # Pydantic or SQLModel schemas
    utils/           # Helpers, no framework deps
  tests/
  Dockerfile
  pyproject.toml
  README.md
Enter fullscreen mode Exit fullscreen mode

I keep pyproject.toml at the service root. It lists only what that service needs. Shared code? That’s a separate problem - I’ll get to it. This layout keeps CI fast and deps predictable. I’ve seen teams try a monorepo with services in folders. It works until someone updates a shared library and breaks three services at once. Avoid that unless you have strong tooling for versioned deploys.

How do services talk to each other without tight coupling?

HTTP is my default. FastAPI makes it easy to build internal APIs. I use httpx.AsyncClient with timeouts and retries. Example:

# in service A, calling service B
import httpx
from app.config import settings

async def get_user_profile(user_id: str):
    async with httpx.AsyncClient(timeout=5.0) as client:
        resp = await client.get(f"{settings.USER_SERVICE_URL}/users/{user_id}")
        resp.raise_for_status()
        return resp.json()
Enter fullscreen mode Exit fullscreen mode

I never expose internal APIs externally. They live behind a service mesh or API gateway. For async work, I reach for Redis Streams or RabbitMQ. Event-driven helps with resilience - if service B is down, service A can keep working and retry later. But it adds complexity. I only use events when I need true decoupling or fan-out. For simple request-response, HTTP is simpler to debug. I’ve been burned by silent message queue failures. Always monitor your consumers.

What’s a practical docker-compose setup for local development?

I version-compose my services. Each gets its own file in /compose. Base file defines shared networks and volumes. Overrides handle dev vs prod.

# compose/base.yml
services:
  user-service:
    build: ../services/user
    ports: ["8001:8000"]
    environment:
      - DATABASE_URL=postgresql://user:pass@db/userdb
    depends_on:
      - db
  # ... other services
Enter fullscreen mode Exit fullscreen mode

I mount the service code as a volume so changes reload locally. In prod, I bake the code into the image. Never do pip install -e . in production Dockerfiles. It’s a security risk and breaks reproducibility. I’ve seen teams do it to skip rebuilds. Don’t. The five-second save isn’t worth the risk.

How do you handle shared libraries and versioning?

I avoid them when possible. If two services need the same validation logic, I copy it. Yes, duplication. But it means Service A can upgrade its copy without breaking Service B. When sharing is unavoidable - I’ve done it for internal SDKs or protobuf definitions - I publish to a private PyPI index. Each service pins an exact version. I use Dependabot to alert on updates. I never use >= or ~= in requirements for shared libs. Pinning prevents surprise breakage. I’ve lost hours debugging why a service started failing after a “minor” patch to a shared util. Trust me, pin everything.

What does monitoring and logging look like across services?

I structure logs as JSON. Every line includes service, trace_id, timestamp, and level. I use structlog with FastAPI middleware to inject correlation IDs. Example:

# middleware.py
import structlog
from starlette.middleware.base import BaseHTTPMiddleware

class LoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        structlog.contextvars.clear_contextvars()
        structlog.contextvars.bind_contextvars(
            service=request.url.path,
            trace_id=request.headers.get("x-trace-id", "unknown")
        )
        response = await call_next(request)
        return response
Enter fullscreen mode Exit fullscreen mode

I ship logs to Loki or Elasticsearch. Metrics go to Prometheus via prometheus_fastapi_instrumentator. I track latency, error rates, and queue depths. Alerts fire on 99th percentile latency > 500ms or error rate > 1%. I’ve had incidents where logs were unstructured grepping nightmares. JSON logging with trace IDs cut our MTTR by half. Don’t skip this.

What deployment strategies work in production?

I deploy each service independently. CI builds the image, pushes to a registry, then ArgoCD or Flux rolls it out. Canary releases help - I route 5% of traffic to the new version first. If metrics look good, I ramp up. Blue-green is overkill for most of my workloads. I use Kubernetes, but the principle applies to ECS or Nomad. Database migrations? I run them as a separate job before the deploy. Never embed migrations in the app startup. If the migrate fails, you don’t want half your services up and half down. I’ve seen this cause split-brain scenarios. Run migrations first, then deploy.

FAQ

How do I handle shared data between microservices?
Each service owns its database. For cross-service queries, I use API composition or async events. Never direct DB access - it breaks independence.

When should I avoid microservices?
If your team is small, your domain is simple, or you don’t need independent scaling. Start with a monolith. Split when you feel the pain.

What’s the biggest mistake you’ve seen with Python microservices?
Tight coupling through shared libraries or databases. It defeats the purpose. Services must fail independently.

Key Takeaways

  • Structure services as independent owning their code and data
  • Use HTTP with timeouts for sync comms, events for async decoupling
  • Pin shared library versions; prefer duplication over coupling
  • Structure logs as JSON with trace IDs for debugging
  • Deploy services independently; run DB migrations before rollout
  • Monitor latency, error rates, and queue depths with alerts
  • Avoid shared databases and mutable imports across service boundaries TOTAL WORDS: 1248

Top comments (0)