Asynchronous events

How event-driven asynchronous flows work: publishing, consuming, and processing across microservices through the Event Bus.

What is an event-driven asynchronous flow

In an asynchronous flow, a service publishes an event signaling that something happened, and other services react to that event without the publisher needing to wait for a response. There is no direct connection between the producer of the event and its consumer.

This model is fundamental in distributed architectures because it enables temporal decoupling: services don’t need to be available at the same time in order to communicate.

Difference from synchronous communication

AspectSynchronousAsynchronous (events)
CouplingThe sender knows the receiverThe sender doesn’t know who consumes
AvailabilityBoth must be activeThe consumer can process later
ResponseImmediateNo direct response
ScalabilityLimited by the slowest receiverEach consumer scales independently
ComplexityLowerHigher (ordering, duplicates, consistency)

Anatomy of an event

A well-defined event contains:

  • Type: name identifying what happened (OrderPlaced, PaymentCompleted)
  • Payload: data relevant to the fact (order ID, amount, timestamp)
  • Metadata: traceability information (correlationId, timestamp, source)
  • Version: to handle schema evolution
{
  "type": "OrderPlaced",
  "version": "1.0",
  "timestamp": "2024-01-15T10:30:00Z",
  "correlationId": "abc-123-def",
  "source": "ms-orders",
  "payload": {
    "orderId": "order-456",
    "customerId": "cust-789",
    "totalAmount": 150.00,
    "items": [
      { "productId": "prod-001", "quantity": 2 }
    ]
  }
}

The complete flow of an event

sequenceDiagram
    participant MS1 as Microservicio Origen
    participant EB as Event Bus
    participant MS2 as Consumidor A
    participant MS3 as Consumidor B
    participant MS4 as Consumidor C

    MS1->>EB: Publicar evento (OrderPlaced)
    Note over MS1: El publicador continúa su trabajo

    EB->>MS2: Entregar evento
    EB->>MS3: Entregar evento
    EB->>MS4: Entregar evento

    MS2->>MS2: Actualizar inventario
    MS3->>MS3: Enviar notificación
    MS4->>MS4: Registrar analytics

Step 1: Publishing

The origin microservice completes its main operation (for example, creating an order) and publishes an event to the Event Bus. Once published, the service does not wait for anyone to process it.

Step 2: Distribution

The Event Bus receives the event and distributes it to all subscribed consumers. Depending on the technology (Kafka, RabbitMQ, SNS/SQS), distribution can be:

  • Fan-out: every subscriber receives a copy
  • Competing consumers: only one consumer in the group processes each message
  • Combination: fan-out across groups, competing consumers within each group

Step 3: Consumption

Each consumer receives the event and runs its logic independently. A slow consumer doesn’t affect the others.

Step 4: Acknowledgment

The consumer acknowledges that it processed the event successfully. If it fails, the Event Bus can retry delivery.

Publishing patterns

Publish after persisting

The service first saves to its database and then publishes the event. It’s simple but carries a risk: if publishing fails after persisting, the event is lost.

Outbox Pattern

The service saves the event to an outbox table within the same transaction as the business operation. A separate process reads the outbox table and publishes the events to the bus. This guarantees consistency between the operation and the publication.

Event Sourcing

Instead of saving the current state, all the events that produced that state are stored. Publishing is inherent to the model.

Challenges of asynchronous flows

Event ordering

Events can arrive in a different order from the one in which they were published. Consumers must be able to handle this, either by reordering or by being tolerant of out-of-order delivery.

Duplicate events

The Event Bus can deliver the same event more than once (at-least-once delivery). Consumers must be idempotent: processing the same event twice must produce the same result as processing it once.

Eventual consistency

Data across services is not synchronized instantly. After an event is published, some time may pass before all consumers reflect the change. The system must be designed to tolerate this window of inconsistency.

Observability

Tracing an asynchronous flow is harder than a synchronous one. The correlationId is essential for following the chain of events across multiple services.

When to use asynchronous events

They are appropriate when:

  • Multiple services need to react to the same fact
  • No immediate response is needed
  • You want to decouple services to scale independently
  • The process can tolerate eventual consistency

They are not appropriate when:

  • An immediate response is needed for the user
  • The operation requires strong consistency across services
  • The added complexity is not justified by the volume or requirements

Summary

Event-driven asynchronous flows let services communicate without direct coupling. The publisher doesn’t know the consumers, and each consumer processes at its own pace. This brings scalability and resilience, but it introduces challenges of ordering, duplicates, and consistency that must be addressed with patterns such as idempotency, outbox, and distributed tracing.