An event-driven microservice is a microservice designed to interact by means of event streams — and it can be both a consumer of one set of input event streams and a producer of another set of output event streams. Consumer microservices consume and process events from one or more input event streams; producer microservices produce events to output event streams for other services to consume. The event streams are served by an event broker, and communication between event-driven microservices is completely asynchronous.
An event-driven microservice may be stateless or stateful, and it may also contain synchronous request–response APIs alongside its event-driven interfaces — the two styles coexist in the same service and in the same business topology.
Chapter 10 introduced the event-driven microservice among the patterns of the catalogue (event sourcing, sagas, CQRS). This lab note — based on [AB] A. Bellamare, Building Event-Driven Microservices, O’Reilly (2020) — is the deeper treatment promised there: it designs the events themselves, the streams, the contracts and the broker infrastructure.
An event can be anything that has happened within the scope of the business communication structure — domain events: receiving an invoice, booking a meeting room, requesting a cup of coffee, hiring a new employee, successfully completing arbitrary code. Once these events start being captured, event-driven systems can be created to harness and use them across the organization. An event is a recording of what happened, much like how an application’s information and error logs record what takes place in the application — but differently from logs, which are technical, events are considered here the single source of truth, and as such they must contain all the information required to accurately describe what happened.
The topology is an important concept in event-driven microservices, and there are two kinds:
Patterns to support event identification and definition:
Events are typically represented using a key/value format: the value stores the complete details of the event, the key is used for identification purposes (routing and aggregation operations on events with the same key), and the key is not a required field for all event types. There are three main event types:
Entity events are particularly important in event-driven architectures: they provide a continual history of the state of an entity and can be used to materialize the state — only the latest entity event is needed to determine the current state of an entity. A stateful table can be materialized by applying entity events, in order, from an entity event stream: each entity event is upserted into the key/value table (inserting a new row if it doesn’t already exist, or updating it if it does), so that the most recently read event for a given key is represented.
Conversely, a table can be converted back into a stream of entity events by publishing each update to the event stream — this is the table–stream duality property, fundamental to the creation of state in an event-driven microservice. The two directions together close the loop that keeps streams and materialized state consistent.
Event-driven microservices make event data important in two roles: as a means of long-term and implementation-agnostic data storage, and as a means of communication between services. Therefore producers and consumers must have a common understanding of the meaning of the data — ideally, the consumer must be able to interpret the contents and meaning of an event without consulting the owner of the producing service. This requires a common language for communication between producers and consumers: event-driven data contracts, analogous to an API definition between synchronous request–response services.
A well-defined data contract has two components:
The way to enforce data contracts and provide consistency is to define a schema for each event: the producer defines an explicit schema detailing the data definition and the triggering logic, with all events of the same type adhering to this format — a mechanism for communicating its event format to all prospective consumers, against which consumers can confidently build their business logic.
The schema format must support a full range of schema evolution rules: schema evolution enables producers to update their service’s output format while allowing consumers to continue consuming the events uninterrupted, and an explicit set of rules (the compatibility types: forward, backward, full) goes a long way in enabling consumers and producers to update their applications in their own time. Technologies such as Apache Avro and Google’s Protobuf provide schematization with an evolution framework — where certain sets of changes can be safely made without requiring downstream consumers to make a code change — and language-specific serialisations that generate typed classes to convert the schematized data into plain old objects in different languages.
An important principle in designing event streams:
Each event stream has one and only one producing microservice; this microservice is the owner of each event produced to that stream.
The outcomes of the single writer principle:
At the heart of every production-ready event-driven microservices platform is the event broker: the system that receives events, stores them in a queue or partitioned event stream, and provides them for consumption by other processes. Events are typically published to different streams based on their underlying logical meaning. Event broker systems suitable for large-scale enterprises follow the same model: multiple, distributed event brokers work together in a cluster to provide a platform for the production and consumption of event streams.
Event broker features:
Data storage features:
Event brokers vs message brokers. Event brokers can be used in place of a message broker — but a message broker cannot fulfil all the functions of an event broker. Message brokers enable systems to communicate through publish/subscribe message queues: producers write messages to a queue, a consumer consumes and processes them, and messages are acknowledged as consumed and deleted either immediately or shortly thereafter. Event brokers are designed around providing an ordered log of facts, which requires more complex capabilities. Two specific needs of event brokers are not satisfied by message brokers:
The durable and immutable log provides the storage mechanism for the single source of truth: the event broker becomes the only location in which services consume and produce data, and every consumer is guaranteed an identical copy of the data. Adopting the event broker as the single source of truth requires a culture shift in the organization:
Most event-driven microservices follow, at a minimum, the same three steps — consume an event from an input event stream, process that event, and produce any necessary output events — starting a loop to poll the consumer client for new events and emitting any required output events. Some services derive their input event from a synchronous request–response interaction instead; in stream-sourced services the instance creates a producer client and a consumer client and registers itself with any necessary consumer groups.
Data liberation is the identification and publication of cross-domain data sets to their corresponding event streams — part of a migration strategy for event-driven architectures. Transitioning an organization requires integrating existing systems: migrating means making the necessary business domain data available in the event broker consumable as event streams, sourcing the data from the existing systems and state stores that contain it. Data liberation enforces the two primary features of event-driven architecture — the single source of truth and the elimination of direct coupling between systems — and the liberated event streams allow new event-driven microservices to be built as consumers, with existing systems migrated in due time. One method of liberating data uses a dedicated, centralized framework to extract data into event streams — e.g. Kafka Connect (exclusively for the Kafka platform), Apache Gobblin and Apache NiFi — each allowing a query against the underlying data set with the results piped through to the output event streams. Available event-driven microservices frameworks include Apache Kafka and the Confluent Platform, Spring Event-Driven and the Axon Framework.
An event-driven microservice is designed to interact by means of event streams: it can be both a consumer of input event streams and a producer of output event streams, communicating completely asynchronously through an event broker; it may be stateless or stateful and may also expose synchronous request–response APIs. The microservice topology is the event-driven processing internal to a single microservice (ingest, transform, store, join, emit); the business topology is the graph-like relationship between microservices, event streams and request–response APIs that fulfils complex business functions — extensible by adding services coupled asynchronously through new streams.
An event is anything that has happened within the scope of the business communication structure (a domain event). An event is a recording of what happened: unlike technical logs, events are considered the single source of truth and must contain all the information required to accurately describe what happened. The “Tell the Truth, the Whole Truth, and Nothing but the Truth” pattern requires an event to be the complete description of everything that happened — the resultant data of applying business logic — recorded as an immutable fact, so consumers never need to consult any other source to know that the event took place.
Events use a key/value format: the value stores the complete details, the key serves identification, routing and aggregation (and is not required for all types). Unkeyed events are singular statements of fact with no key; entity events describe the properties and state of an entity at a point in time, keyed on the unique ID of the business thing (e.g. a book keyed on ISBN); keyed events contain a key but do not represent an entity — they are used for partitioning the stream to guarantee data locality within a single partition.
Entity events provide a continual history of the state of an entity; only the latest entity event is needed to determine its current state. A stateful table is materialized by applying entity events, in order, from an entity event stream, upserting each event into the key/value table (insert if new, update if present). Conversely, a table can be converted into a stream of entity events by publishing each update to the event stream — the table–stream duality property, fundamental to creating state in an event-driven microservice.
Data contracts are the common language between producers and consumers of events — analogous to an API definition between synchronous services — comprising the data definition (what will be produced: fields, types, structures, i.e. the event schema) and the triggering logic (why it is produced: the business logic that triggered creation). Contracts are enforced by defining an explicit schema for each event; consumers build their business logic against the schematized data. Schema evolution is supported through compatibility types (forward, backward, full) with technologies such as Avro and Protobuf.
Each event stream has one and only one producing microservice, which owns every event produced to that stream. This guarantees that the authoritative source of truth is always known for any given event, permitting the tracing of data lineage through the system; access control mechanisms should enforce ownership and write boundaries.
Event broker features: scalability (add instances to grow capacity), durability (data replicated between nodes), high availability (clients reconnect to other nodes on failure), high performance (hundreds of thousands of reads/writes per second shared across nodes). Data storage features: partitioning (parallel substreams for throughput), strict ordering (per partition, in publish order), immutability (no modification once published), indexing (offsets; consumer lag as a scaling metric), infinite retention, and replayability (any consumer can read whatever data it requires).
Two specific needs are not satisfied. First, message brokers provide only queues: consumers sharing a queue each receive only a subset of the records, so state cannot be communicated correctly via events; event brokers maintain a single ledger of records with per-consumer indices, so every consumer can access all events. Second, message brokers delete messages after acknowledgment, while event brokers retain events indefinitely in an immutable, append-only log, enabling replay from anywhere at any time — the basis of the single source of truth.
The microservice consumes an event from an input event stream, processes it, and produces any necessary output events, looping to poll the consumer client for new events. The key function is processEvent, which encapsulates the business logic and possibly emits events — the entry point to the processing topology of the microservice. At-least-once processing is typically achieved by committing offsets after producing the output. Input events may also derive from synchronous request–response interactions.
Data liberation is the identification and publication of cross-domain data sets to their corresponding event streams, part of a migration strategy for event-driven architectures. It enforces the single source of truth and the elimination of direct coupling between systems; liberated streams let new event-driven microservices and reactive frameworks consume the data while existing systems are migrated in due time. Centralized extraction frameworks: Kafka Connect, Apache Gobblin, Apache NiFi. Event-driven microservices frameworks: Apache Kafka/Confluent Platform, Spring Event-Driven, Axon Framework.