Python Event-Driven Architecture — Core Concepts
What Event-Driven Architecture Is
Event-Driven Architecture (EDA) is a design pattern where services communicate by producing and consuming events — records of things that happened. Instead of direct service-to-service calls, services publish events to a broker, and interested services subscribe to those events.
The producer doesn’t know (or care) who consumes the event. This decoupling is the foundation of EDA.
Events vs Commands
Two types of messages flow through event-driven systems:
Events describe something that already happened. They’re facts:
OrderPlaced— an order was placedPaymentProcessed— a payment went throughInventoryReserved— stock was reserved
Commands request that something happen. They’re instructions:
ChargePayment— please charge this cardSendEmail— please send this emailShipOrder— please ship this order
Events are broadcast to all interested subscribers. Commands are sent to a specific handler. Most EDA systems use both: an event triggers a command handler, which produces new events.
The Event Broker
The broker is the central nervous system. It receives events from producers and delivers them to consumers.
| Broker | Best For | Python Library |
|---|---|---|
| RabbitMQ | Task queues, command routing | aio-pika, pika |
| Apache Kafka | High-throughput event streaming, replay | confluent-kafka, aiokafka |
| Redis Streams | Lightweight event streaming | redis-py |
| AWS SNS/SQS | Cloud-native pub/sub | boto3 |
| Google Pub/Sub | Cloud-native, global | google-cloud-pubsub |
RabbitMQ Example
import aio_pika
import json
# Producer
async def publish_order_placed(order_id: str, total: float):
connection = await aio_pika.connect_robust("amqp://guest:guest@rabbitmq/")
channel = await connection.channel()
exchange = await channel.declare_exchange("events", aio_pika.ExchangeType.TOPIC)
event = {
"type": "OrderPlaced",
"data": {"order_id": order_id, "total": total}
}
await exchange.publish(
aio_pika.Message(json.dumps(event).encode()),
routing_key="order.placed"
)
# Consumer
async def consume_order_events():
connection = await aio_pika.connect_robust("amqp://guest:guest@rabbitmq/")
channel = await connection.channel()
exchange = await channel.declare_exchange("events", aio_pika.ExchangeType.TOPIC)
queue = await channel.declare_queue("payment-service.orders", durable=True)
await queue.bind(exchange, "order.placed")
async with queue.iterator() as messages:
async for message in messages:
async with message.process():
event = json.loads(message.body)
await process_payment(event["data"])
Eventual Consistency
In EDA, data across services isn’t instantly consistent. When an order is placed, the order service has the order immediately, but the inventory service might take a few hundred milliseconds to process the reservation.
This is eventual consistency — given enough time, all services converge to the same state. It works because:
- Events are durable (stored in the broker until consumed)
- Failed processing is retried
- The system self-heals
This contrasts with strong consistency in monoliths, where a single database transaction ensures everything updates atomically. EDA trades immediate consistency for independence and resilience.
Event Schemas
Events need clear contracts. Use Pydantic for schema validation:
from pydantic import BaseModel
from datetime import datetime
from uuid import UUID, uuid4
class OrderPlacedEvent(BaseModel):
event_id: UUID = uuid4()
event_type: str = "OrderPlaced"
timestamp: datetime
data: OrderPlacedData
class OrderPlacedData(BaseModel):
order_id: str
customer_id: str
items: list[OrderItem]
total_amount: float
currency: str = "USD"
class OrderItem(BaseModel):
product_id: str
quantity: int
unit_price: float
Schema evolution is critical — when you add fields, existing consumers shouldn’t break. Add new fields as optional with defaults. Never rename or remove fields without a migration period.
Choreography vs Orchestration
Two patterns for coordinating multi-step workflows:
Choreography
Each service reacts to events independently. No central coordinator:
OrderPlaced → PaymentService → PaymentProcessed → InventoryService
→ InventoryReserved → ShippingService → ShipmentCreated
Pros: No single point of failure, services are truly independent. Cons: Hard to see the full workflow, difficult to handle failures that span services.
Orchestration
A central orchestrator directs the workflow:
OrderOrchestrator:
1. Send ChargePayment command → PaymentService
2. Wait for PaymentProcessed event
3. Send ReserveInventory command → InventoryService
4. Wait for InventoryReserved event
5. Send CreateShipment command → ShippingService
Pros: Clear workflow visibility, easier error handling. Cons: Orchestrator is a single point of failure and coordination bottleneck.
Most real systems use a mix: choreography for simple reactions, orchestration for complex business workflows.
Common Misconception
“Event-driven means everything is asynchronous and eventually consistent.”
Not necessarily. EDA systems can have synchronous components. An HTTP API that places an order can publish an event synchronously (waiting for broker acknowledgment) and return a response to the user. The downstream processing (payment, shipping) happens asynchronously. The key is that services communicate through events, not that every interaction is fire-and-forget.
The one thing to remember: Event-driven architecture decouples services through events and a broker, trading immediate consistency for resilience and independent scalability — but success requires clear event schemas and a deliberate choice between choreography and orchestration.
See Also
- Python Aggregate Pattern Why grouping related objects under a single gatekeeper prevents data chaos in your Python application.
- Python Bounded Contexts Why the same word means different things in different parts of your code — and why that is perfectly fine.
- Python Bulkhead Pattern Why smart Python apps put walls between their parts — like a ship that stays afloat even with a hole in the hull.
- Python Circuit Breaker Pattern How a circuit breaker saves your app from crashing — explained with a home electrical fuse analogy.
- Python Clean Architecture Why your Python app should look like an onion — and how that saves you from painful rewrites.