My daughter is event-driven
A personal anecdote on what it means to be event-driven, and why you should be thinking that way too when designing systems.
“My cup is empty.”
…exclaims my daughter. My wife and I shoot an irritated glance at each other, waiting for more specific information, and perhaps a ‘please’ too.
Mere seconds later, my daughter then repeats herself in the same manner, not understanding why this request hasn’t been actioned already. After all, she’s made it twice now.
Only, it’s not a ‘request’, is it? ‘My cup is empty’ is a statement. A fact. She clearly has finished her drink and wants a refill, but at no point has that ‘request’ been made.
Instead, an assumption has been made on her behalf that, in broadcasting what’s happened (the empty state of the cup), ‘someone’ knows what to do with that information and they should action it. Immediately.
My wife and I require more than a statement to carry out the required task. But that’s because we are human, and the complexities of humans mean that language should be structured to also satisfy our emotions.
Machines have no such emotional attachment. They don’t need a ‘please’ either. They rely on programming and a trigger to execute their tasks.
This got me thinking. When taken in the context of the IT industry, what my daughter said is actually very efficient. Bypassing the tangles of human social and emotional language, what she did was to broadcast an event.
In this article, we’ll explore the differences between events — things that have happened — and requests — something that needs to be actioned by a specific party.
We’ll also look at the differences between the needs of a human and the needs of a machine and, critically, why we should be designing our systems around the latter, and not the former.
Terminology check…
What is an ‘event’?
- There are a lot of definitions out there but, essentially, it is ‘something that has happened’.
- It may also be described as a ‘fact’, something that is immutable in nature because it has already occurred and therefore cannot be changed.
- In this example, the state of the cup changed from ‘not empty’ to ‘empty’. This state change is an event, and my daughter chose to succinctly broadcast it with “my cup is empty”.
What is a ‘request’?
- A request is a communication between one party and another. The communication could be a specific instruction or it could be just seeking information.
Humans prefer to think in terms of requests
When we humans interact we tend to direct our focus towards other humans. Typically, this will be one party talking to another party, with a specific purpose or goal in mind.
We describe it as a conversation, where two (or more) parties are directly addressing each other and are responding in real-time.
“Daddy, could I have another squash please?”
Oooo, that would have been much nicer, right? If my daughter had said this then it would have made me much happier for a number of reasons:
- In saying “Daddy” she is addressing me directly, establishing a conversation
- In a polite manner, she has made a specific request for a squash fill-up
- She even said please
The structure of this revised request suits a human and hooks into their emotional needs such that they’d be delighted to help.
But, uh oh, what if I’m not available to fulfil the request? Perhaps I’m currently chopping up some chicken. My hands are mucky so she now needs to wait until I’ve finished and washed my hands. Meltdown!
Request-driven systems introduce brittleness
Ok, in reality, the chopping of chicken scenario isn’t really a problem because my wife heard it too and can act upon it on my behalf.
But in a software architecture, this is a problem. Consider the following:
- If the success of two services, say A and B, are linked, then the fate of one relies on the success of the other, and vice versa.
- If service A is dependent on service B to complete a task, but service B isn’t currently available to perform it, then it fails and we need to build in scenarios to gracefully deal with the situation.
- Perhaps service A starts to place ever-increasing demands on service B. Service B would need to scale with service A to avoid service issues.
- If A and B can’t communicate then the data in each may no longer be consistent. Stock could be released where none exists. Prices might be different between product browsing and check-out. Customers might try to collect orders where the store didn’t receive the click & collect notification. All things that would negatively affect customer satisfaction.
- And what if service A needs to make a change, and that change forces a change in service B. The success of the teams that own services A and B are now linked, who might now need to budget, plan, prioritise and coordinate the delivery of this change.
Services A and B could be described as being tightly coupled.
De-coupling systems
This section is inspired by “Designing Event-Driven Systems — Concepts and Patterns for Streaming Services with Apache Kafka” by Ben Stopford
How do we break the dependencies between services and the teams that own the services?
Figure 1 shows a simple system that takes an order from a customer and, using previously-created customer profile information (green), notifies the customer of changes in their order status.
When the customer orders an item, the Orders service creates an order and stores it. The Orders service then calls the Notification service to ask it to inform the customer that the order was created successfully. The Notifications service needs to call the Customer service first to get the contact details of the customer before sending that notification.
For the customer notification to succeed:
- The Orders service must know about the Notifications service
- The Notification service must know about the Customer service
- The Notification and Customer services must be reliable
When I say ‘reliable’, I mean that they are both available and performant within service level agreements. As close to ‘all the time’ as is possible.
Imagine you are in the Orders Team. In the above system, you are not in full control of the success of the service that you own. How can we remove these conditions for success?
Figure 1 demonstrates a request-driven model where the Orders service commands the Notification service to do something, and the Notification service queries the Customer service.
Figure 2 shows the same system but, instead of the Orders service sending commands directly to the Notification service, it is issuing an ‘orderCreated’ event to an event-streaming platform (ESP).
On an ESP, you can store a persistent, ordered ledger of events. This abstracts information away from the inner-workings of a service so that it is democratised. You can read more here: https://developer.confluent.io/patterns/event-stream/event-streaming-platform/
This time the Orders service no longer needs to know about the Notification service. The Orders service just emits an event denoting that the order was created for ‘Customer X’. The Notification service subscribes to the events and, when it receives a new event, calls the Customer service to get the contact details.
For the customer notification to succeed:
- The Notification service must know about the Customer service
- The Notification and Customer services must be reliable
As an added benefit, other systems may now subscribe to order events without the Order service changing anything.
In Figure 3 we further break dependencies by making the Notification/Customer service interaction event-driven. Now, when a customer updates their details in the Customer service, a ‘customerUpdated’ event is sent to the ESP.
The Order service still doesn’t need to know about the Notification service. The Notification service no longer needs to know about the Customer service. The Notification service subscribes to the Customer service events and keeps a local store of updates. Then, when it receives an ‘orderCreated’ event, it searches locally for up-to-date customer information and sends the notification.
For the customer notification to succeed:
- The Notification service must be reliable
Only the Notifications service needs to be reliable here. The Orders and Customer services aren’t responding in real-time, so the updates will eventually get to the Notifications service.
‘Eventually’ here could mean milliseconds, seconds or even minutes. Hopefully, it doesn’t mean hours or days, but even if it did then the architecture means that the services aren’t dependant on one another and would just pick up from where they left off. This is known as ‘eventual consistency’.
The benefits of event-driven systems
Instead of getting one system to ask another system to perform a specific task (request-driven), what we did here is to only emit information pertaining to something that happened (event-driven), and to let interested parties (consumers) be in charge of their own destiny.
The evolution of the order notification system demonstrated how the same services — when transformed from a request-driven model to an event-driven model — benefitted from a decoupled architecture.
Each service can now:
- Operate in near real-time, with low latency, as no system is actively waiting on the other system to respond.
- Operate independently from the other, as can the teams that own them.
- Be scaled independently, as required.
- Change at their own cadence.
- Be consumed by many parties without needing the requestor to set up new integrations.
- Benefit from a system that is more resilient to failure.
- Catch-up seamlessly after a failure in the system.
My daughter is event-driven
Just for fun, I imagine Figure 4 is how our ‘home’ system might have looked.
The identification of the state of the drink causes my daughter to emit the ‘myCupIsEmpty’ event. The two consumers of the event, Daddy and Mummy, subscribe to the events.
Daddy, chopping chicken, is currently unavailable, but there is fault tolerance in the system because Mummy is available. When Daddy later becomes available he can act upon it, and then disregard the event when it is noted that the cup is no longer empty.
My daughter doesn’t care who is in the room, she needs only broadcast the event and allow all consumers to pick up the event and act upon it. The system is extensible because other people, perhaps grandparents, may also be in the room to subscribe to pick up the event.
Conclusion
Being humans, it is easy for us to relate to one thing talking to another (a conversation). As such, we feel comfortable designing request-driven systems. It makes sense to us.
In many scenarios, this is still the preferred mechanism. For example, when a service wants to update a datastore it issues a command to a database. It makes no sense to broadcast this as an event as it is a specific command to a specific database.
But by designing systems to talk to each other they need to be aware of each other. And when they are aware of each other their success is co-dependant.
Applying event-driven thinking will often break apart these dependencies and lead to better outcomes where systems are designed around what the machine needs, and not what the human thinks the machine needs.
By using event-driven thinking, my daughter implemented an efficient, extensible, scalable, fault-tolerant, near-real-time system.
But she’s not getting that drink until she asks for it like a human.
If you are interested in joining us on our journey, please check out our careers page.