Reference implementation of the "Thin Events / Rich APIs" integration pattern using Spring Boot and RabbitMQ
This repository contains a reference implementation that demonstrates the “thin events / rich apis” integration pattern
using Spring Boot and RabbitMQ.
There are two applications:
publisher
consumer
The publisher
publishes events when its state changes (e.g.: the collection of entities it manages changes), and theconsumer
consumes those events. Assume that the consumer’s bounded context has some mapping to the publisher’s
bounded context, and the events are how the publisher notifies the consumer of changes in state so that the consumer
can update itself accordingly. For simplicity there is no actual domain implemented to demonstrate the assumed context
mapping.
The published events are “thin” meaning they don’t contain enough information for a consumer to update its own state
accordingly. Instead, the events contain just enough information to inform a consumer of how to get the information it
might need to update its own state. In this example, the event payload contains the URL of the entity that changed, and
a timestamp of when the event occurred. The consumer can make a request to the URL to get the information it needs to
update itself.
The RabbitMQ Fanout Exchange is used so that
the publisher can be certain events will be published to whoever is listening at the moment, even if it’s no one. The
publisher doesn’t care if anyone is listening because its own internal state is consistent; it’s publishing events to be
a good participant in a system, allowing other applications to react to change in its state, very similar to how an
aggregate root publishes domain events.
This implementation also includes patterns to guarantee eventual event publication such as the persistence of events
themselves with publication metadata, the delegation of responsibility of event publication to a background thread, and
RabbitMQ publisher confirms. The consumer’s implementation guarantees eventual reaction to events in a similar way such
as the persistence of received events, the delegation of responsibility of reaction to received events to a background
thread, and consumer acknowledgments.
./gradlew clean build
Run a RabbitMQ instance
docker run -d --hostname rabbitmq --name demo -p 5672:5672 -p 15672:15672 rabbitmq:3-management
Run the publisher
./gradlew :publisher:bootRun
Run the consumer
./gradlew :consumer:bootRun
Make a Thing
to publish an event:
```http request
POST http://localhost:8080/things
Content-Type: application/json
{ “content”: “some-content-000” }
```
Distributed applications are hard to observe. Even in this trivialized example there are two applications, each with
their own HTTP APIs, messages flowing between them brokered by a third application (RabbitMQ), and requests being made
from one to another.
The publisher
and consumer
applications provide three ways to increase observability, and ameliorate maintenance and
operations of the system:
Spring Boot Actuator endpoints related to RabbitMQ:
Each of these metrics can be queried to gain additional insight into application state. For example:
A database web console (for development purposes only). Query the database tables to observe how the state changes
as events are published and consumed.
The functionality of the publisher
and consumer
applications is verified using automated tests that apply two
distinct methodologies:
Using this combination of using narrow integration tests and contract tests, I can be confident of integrating against an external service without ever running tests against a real instance of that service, which greatly eases my build process.
-Martin Fowler
The publisher
application has a suite of integration tests in PublisherApplicationTests
The primary focus of these tests is how information is published.
Note these tests use an embedded Apache Qpid AMQP broker, and that this embedded AMQP broker
is not RabbitMQ. This decision accepts the tradeoff of speed and portability in favor of dev/prod-parity, relying on the
AMQP standard as a mitigation. The embedded nature of the broker also obviates the need for a dedicated AMQP broker,
network connectivity for automated tests, or additional environmental requirements (e.g.: Docker).
This decision not based on a strongly-held opinion, so let me know if you have alternative ideas!
The consumer
doesn’t have this type of test because all it does is consume information from the publisher
, and that
can be tested using contract tests. A real consumer application with actual functionality would probably need one to
test that functionality.
The publisher
application’s ability to create and publish events is verified by the black-box application tests, but
what about what is published?
The publisher
application contains a contract definition that specifies what is published, and to where. The contract
is used by Spring Cloud Contract at build time to generate contract tests that validate the application fulfills the
terms of the contract. Additionally, the contract itself is built into an additional “stub” jar that can be published
somewhere that consumers can access, so that consumers can use the contract to validate their integrations rather than
needing access to an instance of publisher
.
For details review the publisher
messaging contract
, the generated contract tests
, and observe the generated stubs jar alongside the application jar.
Note the publisher
also has contracts, and tests for its web API so that consumers of the messages/events can also
build a reaction to those events with confidence.
The consumer
application intends to consume events published by publisher
, and consumer
intends to react to the
events by making requests to the publisher
‘s web API. Rather than building these integrations against an instance of
the publisher
, the consumer
can declare a dependency on the publisher
‘s stub jar, download some version of the
contracts, and use Spring Cloud Contract in automated tests to mock the publication of messages and web APIs based on
the contract definition.
For details review the ConsumerStubTests
.