Skip to content

Event-Driven Architecture — Reactive Microservices Communication

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Event. We cover key concepts, practical examples, and best practices to help you master this topic.

Event-driven architecture uses events as the primary mechanism for communication between Microservices, where services produce and consume events to react to state changes asynchronously.

What You'll Learn

By the end of this lesson you will understand event-driven architecture principles, implement event producers and consumers, design event schemas, handle event ordering and deduplication, and build reactive microservices that respond to system changes.

Why It Matters

Event-driven architecture decouples services more completely than Message Queues. Services emit events without knowing or caring which other services consume them. This enables true independence: services can be added, removed, or replaced without affecting others.

Real-World Use

DodaZIP's entire backend is event-driven. When a file is uploaded, the upload service emits a FileUploaded event. The virus scanner, compression service, thumbnail generator, and backup service each listen for this event independently. Adding a new service requires zero changes to existing services.

flowchart LR
    A[Service A] -->|Emits Event| B[Event Bus]
    B --> C[Service B]
    B --> D[Service C]
    B --> E[Service D]
    C -->|Emits Event| B
    subgraph Event Store
        F[Event Log]
    end
    B --- F
    style B fill:#2d3748,color:#fff

Event-Driven Principles

Core concepts of event-driven architecture.

# eda_principles.py
# Event-driven architecture principles

def eda_principles():
    print("Event-Driven Architecture Principles")
    print("=" * 40)
    print()
    
    principles = [
        {
            "principle": "Event as Fact",
            "desc": "Events represent things that have happened. They are immutable facts, not commands.",
            "example": "OrderPlaced (past tense), not PlaceOrder (command)"
        },
        {
            "principle": "Publish Once, Consume Many",
            "desc": "One event can be consumed by multiple services independently.",
            "example": "UserRegistered event consumed by email, analytics, and CRM services"
        },
        {
            "principle": "No Direct Coupling",
            "desc": "Producers do not know consumers. Events go to a broker, not to specific services.",
            "example": "Upload service emits FileUploaded without knowing who listens"
        },
        {
            "principle": "Eventual Consistency",
            "desc": "Services update their own data stores in response to events. Consistency happens over time.",
            "example": "Order service confirms order first, then emits event. Inventory updates asynchronously."
        },
        {
            "principle": "Event Schema Evolution",
            "desc": "Events must be versioned to allow schema changes without breaking consumers.",
            "example": "UserRegistered.v1, UserRegistered.v2 with additional fields"
        },
    ]
    
    for p in principles:
        print(f"{p['principle']:30s}")
        print(f"  {p['desc']}")
        print(f"  Example: {p['example']}")
        print()

eda_principles()

Implementing Events

Designing and publishing events.

# event_implementation.py
# Event producer and consumer

def event_implementation():
    print("Event Implementation")
    print("=" * 40)
    print()
    
    event_schema = """
# Event schema design

{
  "event": {
    "id": "evt_abc123def456",      # Unique event ID
    "type": "order.placed",         # Hierarchical event type
    "version": 1,                   # Schema version
    "timestamp": "2026-06-28T10:30:00Z",  # ISO 8601
    "producer": "order-service",    # Source service
    "trace_id": "trace_xyz789",     # Distributed tracing ID
    "data": {                       # Event-specific payload
      "order_id": "ORD-12345",
      "customer_id": "CUST-678",
      "total": 59.99,
      "items": [
        {"product_id": "PROD-1", "quantity": 2}
      ]
    }
  }
}
"""
    print("Event Schema:")
    print(event_schema)
    
    producer_code = """
import json, uuid, datetime

def publish_event(event_bus, event_type, data, trace_id):
    event = {
        "id": str(uuid.uuid4()),
        "type": event_type,
        "version": 1,
        "timestamp": datetime.datetime.utcnow().isoformat() + "Z",
        "producer": "order-service",
        "trace_id": trace_id,
        "data": data
    }
    
    # Publish to event bus (Kafka/RabbitMQ/etc)
    event_bus.publish("order.events", json.dumps(event))
    print(f"Published: {event_type} ({event['id']})")
    return event

# Usage
publish_event(
    kafka_producer,
    "order.placed",
    {"order_id": "ORD-12345", "total": 59.99},
    "trace_xyz789"
)
"""
    print("Producer:")
    print(producer_code)

event_implementation()

Event Consumer Patterns

Different ways services consume events.

# consumer_patterns.py
# Event consumer implementation

def consumer_patterns():
    print("Event Consumer Patterns")
    print("=" * 40)
    print()
    
    consumer_code = """
import json

def handle_event(event_json):
    event = json.loads(event_json)
    event_type = event["type"]
    
    # Route by event type
    handlers = {
        "order.placed": handle_order_placed,
        "order.shipped": handle_order_shipped,
        "order.cancelled": handle_order_cancelled,
        "payment.received": handle_payment_received,
    }
    
    handler = handlers.get(event_type)
    if handler:
        try:
            handler(event["data"], event["trace_id"])
            print(f"Processed: {event_type}")
        except Exception as e:
            print(f"Failed to process {event_type}: {e}")
            # Re-raise to trigger retry/DLQ
            raise
    else:
        print(f"Unknown event type: {event_type}")

def handle_order_placed(data, trace_id):
    print(f"Order placed: {data['order_id']}, amount: {data['total']}")
    # Update local inventory count
    # Send confirmation email
    # Notify shipping service

def handle_order_shipped(data, trace_id):
    print(f"Order shipped: {data['order_id']}, carrier: {data['carrier']}")

# Kafka consumer loop
def consume_loop(consumer, topic):
    consumer.subscribe([topic])
    for message in consumer:
        handle_event(message.value)
"""
    print("Consumer with event routing:")
    print(consumer_code)

consumer_patterns()

Event Ordering and Deduplication

Handling out-of-order and duplicate events.

# ordering_dedup.py
# Event ordering and deduplication

def ordering_dedup():
    print("Event Ordering and Deduplication")
    print("=" * 40)
    print()
    
    dedup_code = """
import redis

class Deduplicator:
    """Idempotent event processing."""
    
    def __init__(self, redis_client):
        self.redis = redis_client
        self.processed_window = 300  # 5 minutes
    
    def is_duplicate(self, event_id):
        """Check if event already processed."""
        return self.redis.exists(f"processed:{event_id}")
    
    def mark_processed(self, event_id):
        """Mark event as processed with TTL."""
        self.redis.setex(
            f"processed:{event_id}",
            self.processed_window,
            "1"
        )
    
    def process_once(self, event):
        if self.is_duplicate(event["id"]):
            print(f"Skipping duplicate: {event['id']}")
            return False
        
        # Process event
        self.mark_processed(event["id"])
        return True

# Usage
dedup = Deduplicator(redis_client)
if dedup.process_once(event):
    # Do the actual work
    update_inventory(event["data"])
"""
    print("Deduplication:")
    print(dedup_code)
    print()
    
    ordering_code = """
# Handling out-of-order events

def handle_order_event(event):
    # Store event in local event store
    store.save_event(event)
    
    # Check if we can apply it
    sequence_number = event["data"]["sequence"]
    last_applied = store.get_last_sequence()
    
    if sequence_number == last_applied + 1:
        # Events are in order, apply directly
        apply_event(event)
    elif sequence_number > last_applied + 1:
        # Gap detected, wait for missing events
        print(f"Gap detected. Expected {last_applied + 1}, got {sequence_number}")
        # Missing events will be applied when they arrive
    else:
        # Duplicate, skip
        print(f"Duplicate event: {event['id']}")
"""
    print("Ordering:")
    print(ordering_code)

ordering_dedup()

Common Mistakes

  1. Using events as commands: Events describe past facts (OrderPlaced), not future intentions (PlaceOrder). Commands should be sent via request-response or command queues.

  2. No event versioning: Without schema versioning, changing an event field breaks all consumers. Always include a version field and maintain backward compatibility.

  3. Assuming FIFO ordering: Most event brokers do not guarantee global ordering. Design consumers to handle out-of-order events using sequence numbers.

  4. Not handling duplicates: Network issues cause duplicate event delivery. All event consumers must be idempotent using event ID deduplication.

  5. Cascading event storms: One event triggering another service that emits another event can create infinite loops. Implement event TTL and cycle detection.

Practice Questions

  1. What distinguishes event-driven from message queue communication? Events are published facts consumed by any interested service. Message queues deliver tasks to specific workers.

  2. Why must events be immutable? Events represent things that have happened. Past facts cannot be changed. If data changes, emit a new event.

  3. What is an event schema version? A number in the event payload that indicates the schema version, allowing consumers to handle different formats.

  4. How do you prevent duplicate event processing? Store processed event IDs with TTL in a fast data store (Redis) and check before processing.

  5. Challenge: Design an event-driven system for a food delivery platform. Define the events (OrderPlaced, RestaurantAccepted, DriverAssigned, Delivered), the services that produce and consume each, and how you handle out-of-order events.

FAQ

What is event-driven architecture?

An architecture where services communicate through events: immutable facts about things that happened. Services emit events without knowing who consumes them.

How is EDA different from message queues?

Message queues deliver tasks to workers (one consumer). EDA broadcasts events to all interested consumers. Events are facts, not commands.

What is eventual consistency in EDA?

Services update their own data in response to events, but each service updates independently. The system becomes consistent over time, not instantly.

How do you handle event schema changes?

Use schema versioning. Add fields as optional. Maintain multiple versions until all consumers migrate. Use schema registries (Avro, Protobuf) for compatibility checks.

Can event-driven systems have synchronous interactions?

Yes. Hybrid architectures use sync calls for queries and immediate actions, and events for state changes that multiple services care about.

Mini Project

Design an event-driven architecture for a blog platform. Define events for PostCreated, PostPublished, CommentAdded, UserFollowed. Implement a producer (blog service), and two consumers (notification service sends emails, analytics service tracks engagement). Include event schema, deduplication, and error handling.

def blog_event_design():
    print("Blog Platform - Event-Driven Design")
    print("=" * 40)
    print()
    print("Events:")
    print("  PostCreated     - Blog service emits when author writes")
    print("  PostPublished   - Blog service emits on publication")
    print("  CommentAdded    - Blog service emits on new comment")
    print("  UserFollowed    - User service emits when follow happens")
    print()
    print("Services & Subscriptions:")
    print()
    print("  Notification Service:")
    print("    - PostPublished -> Email followers")
    print("    - CommentAdded  -> Email post author")
    print("    - UserFollowed  -> Email followed user")
    print()
    print("  Analytics Service:")
    print("    - PostCreated   -> Track writing frequency")
    print("    - PostPublished -> Track publish metrics")
    print("    - CommentAdded  -> Track engagement")
    print()
    print("  Search Index Service:")
    print("    - PostPublished -> Update search index")
    print("    - PostCreated   -> Index draft")
    print()
    print("No service knows about other services.")

blog_event_design()

What's Next

Next: Event Sourcing for event-sourced Microservices Patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro