Event Driven Architecture vs Microservices: When to Use Which
Short answer: Event-driven architecture focuses on asynchronous message flow, while microservices split a system into independently deployable services that usually talk over HTTP. Both solve scalability and organisational challenges, but they do it in different ways and with different failure modes.
In the next few hundred lines I’ll define each style, contrast them, show real-world patterns, and give you Python snippets you can drop into a FastAPI project today. I’ll also flag the hidden costs that bite me most often, so you can decide which approach belongs in your next production stack.
What is event-driven architecture?
Event-driven architecture (EDA) is a design where components react to events - immutable facts that something happened. An event is published once, stored (often in a broker), and any number of consumers may pick it up, process it, and optionally emit new events. The flow is asynchronous by default; the producer never waits for a consumer to finish.
Key traits:
- Loose coupling – producers don’t need to know who consumes.
- Scalability – add consumers without touching the publisher.
- Replayability – stored events can be re-processed for debugging or back-fill.
- Eventual consistency – state converges over time, not instantly.
In practice I use Kafka for high-throughput pipelines and Redis Streams for low-latency internal chatter. The broker becomes the system’s nervous system; everything else is just logic that reacts to its pulse.
What is microservices architecture?
Microservices architecture breaks a monolith into a suite of small, independently deployable services. Each service owns its data, its API contract, and its runtime environment. Communication is usually synchronous (HTTP/REST, gRPC) but can also be asynchronous via a message bus.
Core ideas:
- Bounded contexts – each service models a specific business capability.
- Independent deployment – you can roll out a change without touching the rest.
- Technology heterogeneity – different services can use different languages or databases.
- Failure isolation – a crash in one service shouldn’t bring the whole system down.
I’ve run dozens of FastAPI-based microservices in production; the pattern works well when the domain can be cleanly divided and you have the ops budget to run multiple containers, health checks, and service meshes.
How do event-driven architecture and microservices differ?
The biggest difference is communication style. In EDA, the contract is an immutable event schema; in microservices, the contract is an API request/response model. That leads to distinct trade-offs:
| Aspect | Event-driven | Microservices |
|---|---|---|
| Coupling | Very loose; add a consumer without touching the publisher. | Tighter; every API call creates a runtime dependency. |
| Latency | Usually higher because of broker round-trip and async processing. | Lower for simple request/response paths. |
| Observability | Need to trace events across topics; replay helps debugging. | Trace HTTP calls; simpler but can be noisy with many services. |
| Data consistency | Eventual; you must design compensating actions. | Can be strong if each request updates a single service’s DB. |
| Operational cost | Broker cluster, storage of events, retention policies. | Multiple containers, service discovery, load balancers. |
| Team autonomy | High; teams can own event producers or consumers independently. | High; each team owns a service end-to-end. |
When you mix the two (microservices that publish/consume events) you get the best of both worlds, but you also inherit the combined complexity.
When should I pick event-driven over microservices (or vice versa)?
Pick event-driven architecture when:
- You need to fan-out work to many independent workers (e.g., image processing, model inference).
- Data arrives as a stream and you must guarantee no loss (financial tick data, IoT telemetry).
- Business processes span multiple domains and you want a single source of truth for “what happened”.
Pick microservices when:
- Your domain can be cleanly split into bounded contexts with well-defined APIs.
- Low latency is a hard requirement (e.g., real-time pricing engine).
- You have the ops bandwidth to manage many services, health checks, and versioned APIs.
Don’t force a hybrid if you only have a handful of services and a simple queue would do. Adding a broker just to “future-proof” the design often adds unnecessary operational debt.
Real-world examples and integration patterns
1. Order processing pipeline
- Event-driven: An
order_createdevent lands on Kafka. A payment service consumes it, attempts a charge, and publishespayment_succeededorpayment_failed. A shipping service listens forpayment_succeededand creates a shipment. If any step fails, a compensatingorder_canceledevent rolls back the process. - Microservices: The frontend calls the order service via HTTP, which internally calls the payment service synchronously, then the shipping service. If payment fails, the order service returns an error immediately.
The event-driven version tolerates spikes in order volume because each consumer can autoscale independently. The microservice version is simpler to reason about for a low-traffic shop.
2. Feature flag rollout
- Publish a
feature_flag_updatedevent whenever a flag changes in the admin UI. - All services subscribe; they update their in-memory cache without restarting.
- No need for a central config server hitting every service on each request.
3. AI inference as a service
- A FastAPI endpoint receives a request, writes the payload to a
inference_requestedtopic, and returns a 202 Accepted. - Workers pull the event, run the model, and publish
inference_completed. - The original client polls a
/status/{id}endpoint or receives a webhook.
This pattern decouples the heavy GPU work from the HTTP layer and lets you scale workers separately. See my post on building a robust RAG pipeline for more details.
Best practices for Python implementations
Below is a minimal, production-ready skeleton that shows both styles side-by-side. I use FastAPI, Pydantic, and Redis Streams for the event bus; you can swap Kafka with a few lines.
from fastapi import FastAPI, BackgroundTasks, HTTPExceptionfrom pydantic import BaseModel, Fieldimport redisimport jsonimport uuid
app = FastAPI()r = redis.Redis(host="redis", port=6379, decode_responses=True)
STREAM = "orders"
class OrderIn(BaseModel): user_id: int = Field(..., gt=0) amount: float = Field(..., gt=0)
# ------------------- Event‑driven endpoint -------------------@app.post("/orders/async")async def create_order_async(order: OrderIn, bg: BackgroundTasks): """ Publishes an order_created event and returns immediately. """ event = { "id": str(uuid.uuid4()), "type": "order_created", "payload": order.dict(), } # Write to Redis Stream; * means auto‑generated ID r.xadd(STREAM, event) return {"order_id": event["id"], "status": "queued"}
# ------------------- Synchronous microservice endpoint -------------------@app.post("/orders/sync")async def create_order_sync(order: OrderIn): """ Calls the payment microservice synchronously and returns the result. """ # Simulate an HTTP call to payment service payment_ok = await call_payment_service(order) if not payment_ok: raise HTTPException(status_code=402, detail="Payment failed") # Persist order in DB (omitted) return {"order_id": str(uuid.uuid4()), "status": "confirmed"}
async def call_payment_service(order: OrderIn) -> bool: # In real code use httpx or aiohttp; here we just fake it return order.amount < 1000 # simple rule for demoConsumer worker (event-driven)
import redis, json, time
r = redis.Redis(host="redis", port=6379, decode_responses=True)STREAM = "orders"
def process_payment(event): payload = json.loads(event["payload"]) # Insert real payment logic here success = payload["amount"] < 5000 result = { "type": "payment_succeeded" if success else "payment_failed", "order_id": event["id"], "payload": payload, } r.xadd("payments", result)
while True: msgs = r.xread({STREAM: "0"}, block=5000, count=10) if not msgs: continue for _, entries in msgs: for entry_id, entry in entries: process_payment(entry) time.sleep(0.1)Key takeaways from the code
- Keep event payloads small and serializable (JSON works, but Avro/Protobuf help with schema evolution).
- Use a background task or separate worker process; never block the FastAPI request thread.
- Store the event ID in your DB as the source of truth; it lets you replay safely.
- For microservice calls, wrap HTTP clients with retries and circuit-breakers (see the
httpxdocs).
Observability tips
- Tracing – instrument both producers and consumers with OpenTelemetry. Tag events with
correlation_idso you can stitch a timeline across services. - Dead-letter queues – configure a separate stream for events that repeatedly fail. Don’t let a poison pill stall the whole pipeline.
- Schema registry – if you move to Kafka, a Confluent-style registry prevents breaking changes.
When NOT to use this pattern
- If your team can’t maintain a broker, the whole system becomes a single point of failure.
- When you need sub-millisecond latency (high-frequency trading), the extra hop kills you.
- If you only have one consumer, a simple task queue (Celery) may be enough.
FAQ
Q: Can I run both patterns in the same codebase?
A: Absolutely. Many teams expose HTTP endpoints for CRUD while publishing domain events for downstream processing. Just keep the responsibilities clear: APIs for request/response, events for side-effects.
Q: Do I need a separate database for each microservice?
A: Not strictly, but sharing a DB creates hidden coupling. If you store data in a single relational DB, treat it as a shared resource and accept the risk of cross-service transactions.
Q: How do I version event schemas?
A: Use a schema registry or embed a version field in the event payload. Consumers should be tolerant of unknown fields and reject events with unsupported versions.
Q: What monitoring metrics matter most?
A: For brokers: lag, consumer throughput, and error rate. For services: request latency, error ratio, and circuit-breaker state.
Key Takeaways
- Event-driven architecture = async, loosely coupled, eventual consistency; microservices = sync API contracts, independent deployment, often strong consistency.
- Choose EDA for fan-out workloads, high-volume streams, or when you need replayability. Choose microservices for low-latency, clear bounded contexts, and when you can afford multiple containers.
- Mixing both is common: microservices publish events, other services consume them.
- In Python, FastAPI + Redis Streams or Kafka gives a clean separation; keep workers out of the request thread and use OpenTelemetry for tracing.
- Watch operational costs: broker clusters, schema management, and dead-letter handling can become the hidden budget line items.
Happy building, and feel free to drop a comment if you run into a specific edge case.
Working on something similar?
If you're building backend or AI systems and want a second set of senior eyes, let's talk.