When we were planning the architecture for www.bytethisstore.com, particularly for the backend, we considered a variety of approaches for handling different types of events for different types of actions. For example, when a store purchase occurs, a number of actions need to take place, including:
- validating the cart contents
- adding the record to our database
- sending an email to the customer
- sending the request to begin manufacturing
The question is: what is the best way to coordinate all of these actions in a manner that is clean and maintainable? We considered the pros and cons of using a fully event-driven based system, where each action emits an event with a payload of data and each aspect would subscribe to the event and handle the event accordingly. This is a valid and scalalable approach, and like any other valid approach, has its benefits and detractors. We decided to take a hybrid approach of using an event-driven system for certain types of actions only.
Primary and Secondary Events
In our system, we implicitly handle two types of events:
- Primary Event: an event which must take place immediately and whose result is critical or important to the current task being executed.
- Secondary Event: an event which should take place as a result of some task but whose status or output is not as important to the current task.
Instead of using a fully-fledged event driven architecture, we chose to use the following approach: primary events will be executed immediately within the context of the tasks execution, and secondary events will occur via event dispatch + subscription. For example, if we setup an endpoint to add an item to the cart, the primary event will take place right within the endpoint, and any secondary events not critical to the task will occur via event dispatch + subscription.
Dispatching Events
A standard event emitter service or subject property will be sufficient for dispatching and subscribing to events. However, if you want to make sure all callbacks / subscriptions are handled truly asynchronously, you will need to use or introduce some logic to dispatch the event asynchronously, or fire the subscription asynchronously. The main driving concept behind our approach is the implicit primary vs secondary events/actions; the event dispatching itself is not complex. However, we have decided to use our own event emitter type implementation in order to facilitate our own use cases and conform to some of our coding standards. The section at the bottom of this page outlines our code implementation.
Pros and Cons
This approach, like any other, has its benefits and its drawbacks:
- Pros:
- We can add an arbitrary amount of functionality in response to some event without breaking code where the event is generated.
- We can remove non-critical functionality from critical functionality. For example, we shouldn't make certain endpoints fail just because the system failed to send an email or write a log.
- In our implementation, we can subscribe via RxJS observables, thus following the same format and syntax as other Observables.
- Cons:
- This is not a fully fledged event/message driven system; thus it cannot be utilized like one.
- The distinction between primary and secondary events can become arbitrary.
If we wanted to turn this into a proper event/message driven system, we would need to seperate the event dispatching from the event subscribing. We could have our event dispatcher write new events to some database, then have a separate system receive an update when an event is emitted, read the data, and dispatch accordingly. From the consumer's perspective, they would utilize one service to dispatch events and another to read them. (There are of course many ways to implement this, the method just outlined is a simple example.)
Event Emitter + Observable Impelmentation
In our implementation, we have created a service which allows the client to dispatch an event and consumers to subscribe via observable subscriptions. For simplicity, our service does not provide alternatives such as addEventListener
or on
. The code boxes below show the service, type definitions, decorators, and spec file.