This is a small proof of concept on how you can model event-driven choreography with AMQP for a microservice-based architecture.
Context: Microservices strive for decoupling. However, they require standardization and automation in order to be a valid approach in the real world. The communication between microservices is one topic of this standardization. We propose to design an API style guide that every service team agrees to. The following thoughts can be a basis on the event-driven part of a API style guide. However, this is no generalizable solution but should be adapted to the project context. In a further step these style guides can and should be enforced by automated tests or linting (this step is not covered in this little tutorial).
Roles:
- Publishing Microservice Producer responsible for domainA .
- Consuming Microservice B Consumer responsible for domainB and interested in domain events of domainA.
- Domain model changed events, e.g.
domainA_created
,domainA_updated
, anddomainA_deleted
- Domain action events, e.g.
domainA_action_executed
anddomainA_action_failed
- Producer: publishing events, either directly to queues or to exchange.
- Consumer: consuming events from queues.
- Exchange: post-out box, various types for different publishing rules to queues, e.g. fan-out, direct, topic.
- Queue: FIFO-queue for consumers.
Context:
- Microservices are meant to decouple by using AMQP.
- Microservices want to subscribe only to events they are interested in.
Producer Responsibilities:
- The producer should only be responsible to publish events, making the event type somehow explicit.
- Should not know which consumers exist.
Consumer Responsibilities:
- The consumer should only be responsible to consume events and being able to subscribe to events of interest.
- Consumers want to run in multiple replicas and consume events only once. They build a consumer group.
- Should not know which producers exist.
Exchanges:
- There is one public topic-type exchange responsible for publishing all events under a certain topic.
- Each producer publishes its events over the global exchange under a good topic (see further down).
- As alternative, we could also use one exchange per domain / bounded context. But until we run into performance issues, we should keep it simple and use a global exchange. Naming it according to a microservice is a bad idea because this would introduce coupling by the need to know about the existance of another microservice. Keep it on domain-level instead.
Queues:
- One microservice type should have a queue per domain of interest that guarantees FIFO for all instances of its type. This also ensures all instances of a microservices representing a consumer group. Configure the queue as non-exclusive for that (otherwise you have no competing-consumers)!
- The queue should use the topic mechanism to filter events of no interest.
- The queue connects to the global exchange (or to the matching domain exchange if multiple exist).
- The queue is microservice-specific and, thus, can include the name of the microservice in its name.
Event Topics:
- Topics should be designed hierarchically. We propose somwthing like the following pattern
{domain}.{domainDetail}.{eventType}
. The concrete pattern should be project specific and be agreed on by every microservice team. - An example for a domain model changed event would be
billing.user_information.created
, orbilling.user_information.deleted
. - An example for a domain action event would be
billing.payment.executed
orbilling.payment.failed
.
Events can hold information as payload or as attributes (which is equal to headers). The following questions arise and should be answered project-specifically:
- Do we want to send the whole model/data or just a reference on where to fetch it? (fat vs. thin events)
- What information do we need except the model/data?
- e.g. a correlationId for tracking event flows
- e.g. a timestamp for debuggin (be aware that timestamps are not a good mechanism to construct the order of events)
- e.g. a model/data version. This introduces complexity but enables the evolution of the event API.
- Where do we want to put the additional information? (header/attributes vs. payload)
For our little example we came up with the following event detail design:
headers:
- correlationId: number
- timestamp: date-time
- payloadVersion: semantic-version
payload:
- the model/data that should be sent
See the ./example
directory for the detailled demo project.
We deployed 2 producing microserivce replicas and 10 consuming microservice replicas.
The producing microservices sent messages with a text and a incrementing number. Thus, each message is sent twice, once by each producer:
Meanwhile the consumers compete for the messages, so each message is only consumed once (and not ten times / once per consumer). Thus, each test string is listed twice in the logs (once from each of the two producers).