Event-Driven Core, Request-Response Shell

Posted on 31 August 2024

There’s much uncertainty and doubt (and maybe even fear?) around event-driven architecture. One example is the belief that it’s irrelevant for REST APIs, as using HTTP verbs is quite clearly not event-driven. But behold - you don’t always have to go all-in to win.

Most modern applications provide an HTTP-based API on the server side, usually - more or less - following REST principles. The front end will instruct the backend to do something via a command, and get some response back.

An event-driven architecture, in contrast, is based on systems publishing things that have already happened. Facts. OrderSubmitted. PaymentReceived.

So let’s get that out of the way: For end-user interaction, being purely event-driven is not the right approach. After all, as a user, I want to tell the system to do something. Not give it a notification about what I have done.

The outside view of a typical such system is something like: A service that gets requests via HTTP, does some processing, and manages persistent state with the help of a database.


A simple service


But: In the case of bigger applications, developed by multiple teams, the service the users communicate with via HTTP will most likely not be alone. Other services are going to be involved. So how do these services interact? This can be designed in different ways. They can send requests and expect responses, be it via HTTP, gRPC, or asynchronous messaging.
Or, better: They can emit and listen to events. This is where the event-driven architecture comes into play for web applications and APIs: In the form of event collaboration between the backend services.


Request-Response Service Communication
Don't do this: Request-Response between services.


By using events between the services, you remove a very critical type of coupling: Runtime coupling, sometimes called temporal coupling. In the request-response case, the system A (that the user interacts with) relies on systems B and C to be available and to have the capacity to handle that request at that time. If system B fails, this failure will propagate to system A and it will in turn also be unable to serve requests.

In this case, the goal of the event-driven architecture isn’t that everything is event-driven. It’s that any service that’s exposed to requests that demand a response can handle them on its own. At runtime, it’s not dependent on other services. Any request to it won’t cascade to services it depends on.


Event-Driven Service Communication
Do this instead: Event-Driven Communication.


In a request-response system, the availability of a service is the product of the availability of all services in the call chain. If service A depends (directly or transitively) on four other services, and itself and the four others provide a 99% availability each, the system availability observed from the outside will be 99% to the power of 5, i.e. 95%. If service A has no runtime dependencies, the availability observed from the outside will be that of service A, i.e. in our example 99%.

Core And Shell

If you’ve looked into functional programming, you might have come across the phrase “functional core, imperative shell”.

I’m not sure where this originated - maybe from Gary Bernhardt’s “Boundaries” talk from 2012. What’s this pattern about?

Pure functional code is very pleasant to reason about. It allows you to apply a mathematical mindset. It’s referentially transparent, each function application can be replaced with its result, which is beautiful. Being pure means having no side effects, you only have arguments going in and the result of function application.

As wonderful as freedom from side effects is - creating output or changing the state of data in the database, these are side effects. But these are also things you probably want your program to do. People need to interact with it, and you want to persist state. As Simon Peyton Jones once said, a purely functional program is useless. But instead of giving up on functional programming completely, you can still benefit from it. Just introduce some segregation into your program, into a purely functional core, and a shell that manages the interaction, between that core and the stateful outside world.


Functional Core, Imperative Shell
Functional Core, Imperative Shell. Image by Mario Bittencourt.


Do you see how this translates to event-driven architecture? Go ahead, and have services with request-response interfaces for the users, and for third-party systems that require it. But in between your services, stay “pure”, and stay event-driven.

Bring the data to the process

You’ve probably heard about the troika “command, query, event” when it comes to communication between services. If not, here’s a quick reminder:

Reminder: Commands, Queries, Events
Pattern Event Command Query
Describes.. An event that has happened in the past An intention to perform an operation or change a state A request for information about the current state of one or many objects
Expected Response None A confirmation that the command has been executed, or an error message The requested information

I once heard someone say, and it stuck with me: “Queries are for front-ends”. That makes sense. A service should usually not query another service. It should subscribe to its event stream. We can extend this: Between the services you control, use events. Queries and commands are for the front-end, and for 3rd party and legacy systems that aren’t event-driven.


Event-Driven Core, Request-Response Shell
Event-Driven Core, Request-Response Shell.


Summary

Event-Driven Architecture doesn’t mean every aspect of the system has to be event-driven. In the shell of the system, where it interfaces with users, or with legacy or third-party systems, you can still cater to the reality of request-response driven communication if you have to.
But put events at the core of your system, when it comes to communication between your services. This way, you achieve, among other things, temporal decoupling, increasing the availability of your overall system. There’s still a lot of value in being event-driven, even if it’s restricted to the core.

Categories:
  • Software Architecture, Event-Driven Architecture

Share this article on: