Microservices with Django: Patterns and Anti-patterns

python backend django architecture

“Just use microservices” is terrible advice. Breaking a monolith is hard, and doing it wrong creates a distributed monolith—all the complexity, none of the benefits.

Here’s what actually works with Django.

When to Consider Microservices

Good reasons:

Bad reasons:

Most applications should stay monolithic. Django monoliths can handle enormous scale.

The Monolith First Approach

Start with a modular monolith:

myproject/
├── users/           # Could become service
├── orders/          # Could become service
├── payments/        # Could become service
├── shipping/        # Could become service
└── common/          # Shared utilities

Clear app boundaries. Minimal cross-app imports. This is your extraction roadmap.

Communication Patterns

Synchronous (REST/gRPC)

# Service A calling Service B
import requests

def get_user_orders(user_id):
    response = requests.get(
        f'http://orders-service/api/users/{user_id}/orders/',
        timeout=5
    )
    response.raise_for_status()
    return response.json()

Pros: Simple, familiar, request-response Cons: Tight coupling, cascading failures, latency

Asynchronous (Message Queues)

# Publishing an event
from celery import Celery

app = Celery('orders')

@app.task
def order_created(order_id):
    # Publish event
    publish_event('order.created', {'order_id': order_id})

# Consuming events
@app.task
def handle_order_created(event):
    order_id = event['order_id']
    # Process shipping, notifications, etc.

Pros: Loose coupling, resilience, scalability Cons: Complexity, eventual consistency, debugging difficulty

Data Management

The biggest challenge: shared database or separate?

Shared Database (Anti-pattern… usually)

# Orders service
from users.models import User  # Don't do this!

class Order(models.Model):
    user = models.ForeignKey(User, ...)  # Cross-service reference

This creates hidden coupling. You can’t deploy services independently.

Database per Service

Each service owns its data:

# Orders service
class Order(models.Model):
    user_id = models.IntegerField()  # Just the ID, not a FK
    # Fetch user details via API when needed

You’ll need to handle:

API Gateway Pattern

                    ┌─────────────┐
Browser ───────────▶│ API Gateway │
                    └──────┬──────┘

         ┌─────────────────┼─────────────────┐
         ▼                 ▼                 ▼
   ┌───────────┐     ┌───────────┐     ┌───────────┐
   │   Users   │     │  Orders   │     │ Payments  │
   └───────────┘     └───────────┘     └───────────┘

The gateway handles:

Use Kong, AWS API Gateway, or build with Django.

Service Discovery

Services need to find each other:

# Hard-coded (bad for production)
ORDERS_SERVICE_URL = 'http://orders:8000'

# Environment-based (better)
ORDERS_SERVICE_URL = os.environ.get('ORDERS_SERVICE_URL')

# Service discovery (best for dynamic environments)
# Use Consul, Kubernetes DNS, or similar

Circuit Breaker Pattern

Prevent cascading failures:

import pybreaker

orders_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60
)

@orders_breaker
def get_orders(user_id):
    response = requests.get(f'{ORDERS_URL}/users/{user_id}/orders/')
    response.raise_for_status()
    return response.json()

# Handle open circuit
try:
    orders = get_orders(user_id)
except pybreaker.CircuitBreakerError:
    orders = []  # Graceful degradation

Django-Specific Patterns

Shared Auth with JWT

# Auth service issues JWT
# Other services verify JWT

import jwt

def get_user_from_token(token):
    payload = jwt.decode(
        token,
        settings.JWT_PUBLIC_KEY,
        algorithms=['RS256']
    )
    return payload['user_id']

class JWTAuthenticationMiddleware:
    def __call__(self, request):
        token = request.headers.get('Authorization', '').replace('Bearer ', '')
        if token:
            request.user_id = get_user_from_token(token)
        return self.get_response(request)

Shared Libraries

Extract common code into packages:

shared-django-lib/
├── auth/
├── logging/
├── metrics/
└── utils/

Version carefully. Don’t couple services through shared libraries.

Anti-Patterns to Avoid

Distributed Monolith

Services that must deploy together aren’t microservices.

Chatty Services

Too many inter-service calls indicate wrong boundaries.

Shared Database

Defeats the purpose of independent services.

No Observability

You need tracing, centralized logging, and metrics. More services means more failure modes.

Over-Extraction

Don’t start with 20 services. Extract when pain justifies it.

Observability Stack

Essential for microservices:

# Distributed tracing with Jaeger
from opentelemetry import trace
from opentelemetry.exporter.jaeger.thrift import JaegerExporter

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("get_order"):
    order = orders_service.get(order_id)

Final Thoughts

Microservices solve organizational problems, not technical ones. If you have a small team, a monolith is simpler and faster to develop.

When you do extract services:

The goal is independent deployment and scaling. If you’re not achieving that, reconsider.


Monoliths aren’t the enemy. Complexity is.

All posts