Event-driven architecture

Event-driven architectures often being designed atop message-driven architectures,

An event can be defined as “a significant change in state”.

An event-driven architecture consists of event producers that generate a stream of events, event consumers that listen for the events, and event channels.

Event structure

An event is often used metonymically to denote the notification message itself, which may lead to some confusion.

An event can be made of two parts, the event header and the event body. The event header might include information such as event name, time stamp for the event, and type of event. The event body provides the details of the state change detected. An event body should not be confused with the pattern or the logic that may be applied in reaction to the occurrence of the event itself.

Models

An event driven architecture can use a publish/subscribe (also called pub/sub) model or an event stream model.

  • Pub/sub: The messaging infrastructure keeps track of subscriptions. When an event is published, it sends the event to each subscriber. After an event is received, it can’t be replayed, and new subscribers don’t see the event.
  • Event streaming: Events are written to a log. Events are strictly ordered (within a partition) and durable. Clients don’t subscribe to the stream, instead a client can read from any part of the stream. The client is responsible for advancing its position in the stream. That means a client can join at any time, and can replay events.

In the logical diagram above, each type of consumer is shown as a single box. In practice, it’s common to have multiple instances of a consumer, to avoid having the consumer become a single point of failure in system. Multiple instances might also be necessary to handle the volume and frequency of events. Also, a single consumer might process events on multiple threads.

When to use this architecture

  • Multiple subsystems must process the same events.
  • Real-time processing with minimum time lag.
  • Complex event processing, such as pattern matching or aggregation over time windows.
  • High volume and high velocity of data, such as IoT.

Benefits

  • Producers and consumers are decoupled.
  • No point-to-point integrations. It’s easy to add new consumers to the system.
  • Consumers can respond to events immediately as they arrive.
  • Highly scalable and distributed.
  • Subsystems have independent views of the event stream.

Challenges

  • Guaranteed delivery. In some systems, especially in IoT scenarios, it’s crucial to guarantee that events are delivered.
  • Processing events in order or exactly once. Each consumer type typically runs in multiple instances, for resiliency and scalability. This can create a challenge if the events must be processed in order (within a consumer type), or idempotent message processing logic isn’t implemented.
  • Coordinating messages across services. Business processes often involve multiple services publishing and subscribing to messages to achieve a consistent outcome across a whole workload. Workflow patterns such as the Choreography pattern and Saga Orchestration can be used to reliably manage message flows across various services.

Implementations

Pub/sub

Publish–subscribe is a sibling of the message queue paradigm, and is typically one part of a larger message-oriented middleware system. Most messaging systems support both the pub/sub and message queue models in their API; e.g., Java Message Service (JMS).

RabbitMQ

Apache Kafka

Event streaming

Kafka Streams

Apache Spark

Apache Storm

The API uses a nomenclature convention (e.g. “ActionListener” and “ActionEvent”) to relate and organize event concerns. A class which needs to be aware of a particular event simply implements the appropriate listener, overrides the inherited methods, and is then added to the object that fires the event.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class FooPanel extends JPanel implements ActionListener {
    public FooPanel() {
        super();

        JButton btn = new JButton("Click Me!");
        btn.addActionListener(this);

        this.add(btn);
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        System.out.println("Button has been clicked!");
    }
}

Alternatively, another implementation choice is to add the listener to the object as an anonymous class and thus use the lambda notation (since Java 1.8).

1
2
3
4
5
6
7
8
9
public class FooPanel extends JPanel {
    public FooPanel() {
        super();

        JButton btn = new JButton("Click Me!");
        btn.addActionListener(ae -> System.out.println("Button has been clicked!"));
        this.add(btn);
    }
}

Reference

Event-driven architecture style