Application design through events has been a practice since the late 1980s. We can use events anywhere on the front end or back end. When a button is pressed, some data changes or some back-end action is performed.

But what exactly is the event? When should we use it? What are the disadvantages?

What/When/Why

When classes or components are highly cohesive, their coupling should be low. That is, when components need to call each other cooperatively, let’s say that one component “A” needs to trigger some logic in component “B”, the natural way is to have component A call A method in component B directly. But the premise is that A must know that B exists, so that they are coupled and A must depend on B, which makes the system more difficult to change and maintain. Therefore, events can be used here to prevent this coupling of direct calls.

In addition, component decoupling using events has its own benefits. If we have A work team that is only responsible for component B, then they may not need to communicate with the team responsible for component A and react directly to logical changes in component A in component B. The two component teams can evolve independently (BANQ note: one of the features of microservices) and our applications become more flexible.

Even on the same component team, sometimes we don’t need to execute the result of an action immediately in the same request/response, just asynchronously, such as sending an email. In this case, we can send a response back to the user immediately, send the E-mail asynchronously, and avoid leaving the user waiting for the E-mail to be sent.

However, there are dangers if we use it indiscriminately. We run the risk of logical flows that are conceptually highly cohesive but connected by events that are decouple. In other words, code that should be together will be separated, and it will be hard to follow its flow (similar to a GOto statement) and not easy to understand: it will probably be mixed together like spaghetti!

To avoid turning our codebase into a big pile of spaghetti, we should limit the use of events to explicit cases. In my experience, there are three scenarios for using events:

(1). Decoupled components

(2). Perform asynchronous tasks

(3). Tracking state changes (audit log)

1. Decoupled components (microservices)

When the logic executed by component A needs to trigger the logic of component B, instead of calling it directly, we can send the triggering event to the event dispatcher. Component B listens for specific events in the scheduler and performs actions when the event occurs.

This means that both A and B will depend on the scheduler and events, but they will not know each other exists, and they will be decoupled.

Ideally, neither the dispatcher nor the event should exist between two components:

(1) The dispatcher should be completely independent of our application library and therefore installed in a common location using a dependency management system. In the PHP world, this is something installed in the Vendor folder using Composer and so on.

(2) Events are part of our application and should live between two components that communicate through events (decoupled in structure, coupled in behavior). Events are shared between components and are a core part of the application. Events in DDDS are part of the Shared Kernel. In this way, both components will depend on the shared kernel without being aware of it. In a Monolithic application, however, for convenience, it can be placed in the component that fires the event.

.

DDD shares the kernel

[…]. Clearly define some subset of the domain model that the designated team agrees to share. Keep the kernel small. […]. This explicitly shared thing has a special status and should not be changed without consultation with other teams.

Eric Evans 2014, Domain Driven Design Reference

2. Perform asynchronous tasks

Sometimes we have a logic we want to execute, but it can take quite a long time to execute, and we don’t want the user to wait for it to complete. In this case, you want to run it as an asynchronous job and immediately return a message to the user informing him that the request will be executed asynchronously at a later date.

For example, placing an order at an online store can be done synchronously, but sending a notification email to the user can be done asynchronously.

In this case, what we can do is fire an event that will be queued until a work task can get the event and execute it, as long as the system has resources.

In these cases, it does not matter whether the associated logic is in the same bounded environment; either way, the logic is decoupled.

3. Tracking status changes (Audit logs)

In the traditional way of data storage, we have entities of data. When the data in these entities changes, we simply update the database table row to reflect the new value.

The problem here is that we don’t store why and when these values change.

We can store these changed events in the audit log.

More about this further prospect is explained in terms of event traceability.

Event patterns

Martin Fowler identified three different types of event patterns:

(1) Event notification

(2) Event execution state transfer

(3) Event Sourcing

All of these patterns share the same key concept:

(1) An event is something that happened (happened after something).

(2) The event is broadcast to any code that is listening (code can react to the event).

I. Event notification

Suppose we have an application core with a well-defined component. Ideally, these components are completely separate from each other, but some of their functionality requires some logic to be performed in other components.

The most typical case is described earlier: When the logic executed by component A needs to trigger the logic of component B, instead of calling B directly, A triggers the event and sends it to the event scheduler. Component B listens for specific events in the scheduler and performs actions when the event occurs.

Importantly, one feature of this pattern is that events carry minimal data. It provides just enough data for the listener to know what’s happening and execute its code, usually just the entity ID, or perhaps the date and time the event was created.

advantages

(1) More flexibility: after events are queued, the sender component can continue to execute its own logic, even if an error occurs because they are queued, they can be executed when the error is fixed.

(2) Reduce latency. If events are queued, users do not need to wait for the logic to execute;

Teams can independently develop components that make their work easier, faster, more problem-prone, and more flexible;

disadvantages

(1) If you don’t use standards, you can end up with a bunch of spaghetti code.

2. Event execution status change

Let’s look again at the previous example, where a well-defined component is the core of the application. If some function of component A requires data from other components. The most natural way to get this data is to ask other components, but this means that the component being queried must provide query methods for the component being queried to use. Changing it once or twice doesn’t matter. If the component being queried is frequently asked to provide new query methods, the two components are coupled!

Another way to share data between components is that when a component that owns the data triggers a change event, that event carries the newly changed data. Components interested in this data will listen to these events, retrieve the data from the event, store a local copy of the data, and then react to the new data. This way, when they need external data, they already have it locally, and they don’t need to query other components, nor do they need other components to provide corresponding query methods.

advantages

(1) Greater flexibility, the query component does not depend on the queried component, if the queried component becomes unavailable (due to an error or the remote server is not available), the query component itself can work properly because it has the local data of the master data in the queried component;

(2) Reduce latency because there is no remote call (assuming the component being queried is remote) to access the data;

(3) We don’t have to worry about the load on the component being queried and whether it satisfies all the queries from other query components (especially if it is a remote component);

disadvantages

(1) There will be several copies of the same data, although they are read-only copies and data storage is not an issue at the moment;

(2) Higher complexity of the query component because it will require logic to maintain a local copy of external data, although this is very standard logic. Master/slave consistency.

If the two components in the same process (within the same VM, the same host), the model may not be necessary, but even so, it might be fun, it can be used for decoupling and maintainability, or as these components separated into different micro service work to prepare, maybe sometime in the future we can smooth to upgrade to the service. It all depends on our current needs, our future needs.

3. Event tracing

We assume that an entity is in an initial state. As an entity, it has its own identity that represents a specific thing in the real world that the application models as an entity. The entity data changes over its lifetime, and traditionally the current state of the entity is simply stored in the database as a row record of a table.

(1) Transaction log

That in most cases can be, but if we need to know how to reach the current state of the entity is, that we want to know our bank account credit and debit the amount on every deal, to know the current account balance origin, in this only to save the current state of the traditional way is impossible to achieve, because we only store the current state! Every time, the new balance state overwrites the previous state, for example, the current balance is 10, overwrites the previous balance of 90, how can the account balance have 10 dollars left? If the database doesn’t keep details of transactions, you might think there’s something wrong with the banking system.

Instead of storing Entity state, we focus on storing Entity state changes and calculating Entity state from those changes. Each state change is an event, stored in a stream of events (that is, a table in an RDBMS). When we need the current state of the entity, we calculate it from all the events in the event stream.

The event store becomes the primary source of truth, and the state of the system arises purely from it. For programmers, the best example is version control systems. All committed logs are event stores, and the working copy of the source tree is the system state. — Greg Young, CQRS, 2010

(2) How to delete?

If we find that a state change (event) is an error, we can’t simply delete the event because that would change the state change history, which would violate the whole idea of traceability. Instead, we create an event in the event stream to reverse the event we want to delete. This process, called reversing transactions, not only returns the entity to the desired state, but also leaves a trace showing the object in that state at a given point in time.

Not deleting data also has architectural advantages. Storage systems become add-only architectures, and it is well known that add-only architectures are easier to distribute than updated architectures because there are far fewer locks to handle. — Greg Young, CQRS, 2010

(3) the snapshot

However, calculating entity state can be very expensive when there are many events in the event stream, so to avoid this situation. For every X events we will create a snapshot of the entity state at that time. This way, when we need the entity state, we only need to calculate it to the last snapshot. We can even keep snapshots of updated entities forever, so we balance the two worlds (only states and only events).

(4) Projections

In event gathering, we also have the concept of projection, which is the calculation of events in the event stream, starting at a particular moment. This means that the current state of the snapshot or entity meets the definition of the prediction. But in the prediction of the most valuable ideas in a concept is that we can in a specific analysis of an entity during the period of “behavior”, this allows us to make educated guesses about the future (i.e. if in the past five years, entities have increased activity in August may be the same thing will happen next August), it may be valuable for business.

(5) the pros and cons

Event traceability is very useful for both business and development processes:

1. We query these events for business and development purposes to understand user and system behavior (debugging);

2. We can also use event logs to reconstruct past state, which is useful for both business and development;

3. Automatically adjust the state to deal with traceability changes, which is very suitable for frequent changes required by enterprises;

4. Explore other histories by injecting hypothetical events into reruns, awesome.

But not all is good news, watch out for hidden issues:

1. External updates

When our event triggers an update in an external system, but we are replaying the event to create a projection, we don’t want to refire those events. At this point, when we are in “replay mode,” we can simply disable external updates, or we can encapsulate the logic in the gateway.

Another solution, depending on the actual problem, might be to put external system updates into the buffer and execute them after a certain period of time, ensuring that the event will not be replayed.

2. External query

When our event uses a query to an external system, such as getting a stock rating, what happens when we replay the event to create a projection? We might want to get the same level that was used when the event was first run, perhaps years ago. So remote applications can give us these values, or we need to store them in the system, so we can simulate remote queries by encapsulating the logic in the gateway.

Code changes

Martin Fowler found three types of code changes: new features, bug fixes, and timing logic. The real problem arises when events that should use different business logic rules are played at different points in time. Last year’s tax calculation was different from this year’s. As usual, conditional logic can be used, but it can get messy, so the policy pattern is recommended.

So, I recommend caution and following these rules as much as possible:

1. Keep things stupid and focus on how they change, not how they change. This way we can safely replay any event and expect the result to be the same, even if the business rules change at the same time (although we need to keep the old business rules so that we can apply them when replaying past events);

2. Interactions with external systems should not depend on these events so that we can safely replay the events without refiring the external logic, and we do not need to ensure that the response from the external system is the same as the original event.

Of course, like any other pattern, we don’t need to use it anywhere, it makes sense where we should use it, it gives us an advantage and solves more problems than it creates.

conclusion

Again, this is about encapsulation, low coupling and high condensation.

Events can balance the maintainability, performance and extensibility of code, and event traceability is also the reliability and information that system data can provide.

However, this is a path with its own dangers, as the complexity of concepts and technologies increases, and the misuse of either can have disastrous consequences.