Context
Efficient interservice communication is critical to the performance, and ultimately the success, of any microservice architecture. There are two primary design patterns that implement this communication: synchronous and asynchronous. In synchronous communication, the client sends a message and patiently waits for it to be processed and returned by the server. In asynchronous communication, the client continues to perform their own work while awaiting the response. While synchronous communication is simpler to comprehend and easier to implement, it has some drawbacks; tight coupling between services and scalability issues are particularly noteworthy. Asynchronous communication, on the other hand, is significantly more scalable and offers better decoupling between services.
Event-driven architecture is one example of an asynchronous pattern, where services communicate by producing and consuming events. Changes in data or the state of the producing system are processed as messages by the event consumers. After an event is emitted with a message broker, the consumer receives the information. The sole responsibility of a messenger broker is the delivery of these events, including any retry mechanism.
This blog post will focus on indirect architectures where a producer service sends events to a message broker responsible for routing events, and a consumer service receives events from a message broker. We will outline two approaches to implement this communication: push-based and pull-based.
Event-driven architectures are championed for providing several benefits:
- Decoupling: services are fully decoupled, allowing them to evolve independently
- Scalability: the absence of direct connections means that services can scale independently
- Fault tolerance: message brokers handle failures, ensuring events are delivered to consumers
- Flexibility: consuming services can be added or removed without affecting the other consumers
- Event replay: events can be replayed to recover from failures or to process the events again
- Event sourcing: events can be saved to a log, providing an audit trail of changes in the system
Approaches
Push-based
The push-based approach is a well-suited choice when decoupling, scalability, and speed of delivery are of greatest importance. Decoupling is achieved when the producer does not need to know details about the consumer, and the interaction is handled entirely by the message broker. It's also more scalable, because the producer can send events to the message broker without waiting for the consumer to process and respond. Furthermore, it allows for certain message propagation patterns to be utilized, such as fan-out, fan-in, and point-to-point. The consumer benefits by subscribing to events from multiple different producers, regardless of the languages, frameworks, or technologies they implement. For example, the consumer could expose a language-agnostic REST API that the message broker calls to deliver the events. Since every participant in the pushed event architecture is emitting events as soon as they are ready, the speed of delivery is faster compared to using a pull-based approach.
Pull-based
The pull-based approach is preferred when the consuming service needs to control the rate at which it receives events. The events are queued up within the message broker, and the consumer decides when it is ready to retrieve the next one. This approach is also useful if the consumer needs to process events in a specific order or needs to process events in batches. While this approach requires more awareness of the message broker's language and technology implementation, it provides better control over the rate of event delivery. This is especially beneficial to consuming services prone to overloading, as well as services required to maintain throughput guarantees or adhere to resource consumption bounds. Finally, pull-based approaches are better at handling fault-tolerant scenarios because of the consumer’s ability to pull any events that may have failed to deliver due to a network outage or some other transient error.
Conclusion
Event-driven architectures are a powerful and versatile way to implement communication within a microservices architecture. They provide better scalability, fault tolerance, flexibility, and decoupling between services. Both approaches have their advantages and disadvantages, and the choice between them often depends on the requirements of the system being designed.
At WellSky, we use a push-based approach for integration, orchestration, and interoperability scenarios – such as the IOHub – and prefer a pull-based approach for high-volume data processing and analytics scenarios. When decoupling services use events as a key aspect of event-driven architectures, WellSky has built more scalable, flexible, and fault-tolerant systems that can evolve independently and adapt to changing requirements.