The most used approach for building software today is the request / response mechanism with layered architecture (n-tier) beneath, where the calls propagate vertically through layers. Patterns like MVC, have become very popular and, in a way, standard when people learn and write software.
As layered architecture is the easiest one and can solve many problems, it does not mean that it is the silver bullet for solving all the problems that exist in the software industry. Some of the software can be written more expressively using different design patterns. Layered architecture goes well with small and medium-sized projects. The tricky part of it is to keep everything organized and not make too many layers, or we will end up with Baklava code.
Alternatively, we have event-driven programming which is mostly used in front-end development, where one event can be propagated through the system and many actors can act upon catching that event. Data flow is unidirectional, and adding new features can be done without editing existing components.
While event-driven programming is dominant for building user interfaces, we can use it for writing server-side code too. Good use cases are highly asynchronous systems that do not require an immediate response from the server and use different communication channels to publish progress of a request.
In this tutorial, we will not only dispatch events to demonstrate event-driven programming but also implement CQRS design patterns which divides code that edits the data (commands) from one which is used for reading the data (queries).
Main building blocks of our application will be:
Commands are the actions that will either run business logic or dispatch new events. Events will be used to dispatch other commands. We can have event handlers as well. Query actions and query handlers are responsible for querying (reading) items.
If we imagine a bidding system where one action can trigger other actions in a defined order, and we
want to make it highly asynchronous. We will end up with features like:
Here is a diagram of the flow in our system:
With CQRS module implemented, each event will produce one or more commands, and each command will trigger a new event.
This event-driven system enables the aspect-oriented programming paradigm. Which basically means you can add additional functionality to a software without changing existing functionalities. In our case, it will mean chaining new commands and command handlers with events.
We have chosen Nestjs to implement the described solution for our imaginary bidding system.
Nestjs offers, in its rich ecosystem, CQRS module. Main building blocks of that module are three injectable classes: EventBus, QueryBus, and CommandBus. Each, as the name implies, can trigger either event, query or command.
Reading and writing code for this demo will require learning and diving into Nestjs, as there are many concepts which need to be grasped. Nestjs is a feature-rich framework, which heavily relies on decorators, observables, and it comes with a module system (similar to the one from Angular), dependency injection, inversion of control and etc.
I’ll try to highlight only the important bits from the code, otherwise, this article will be too long. At the bottom of it, you will find a link to a Github repository with all the code and working demo. Here is the directory structure:
From the main controller (and main route /) we will dispatch BidEvent. In Nestjs, controllers are the route handlers.
@Controller() export class AppController { constructor(private readonly eventBus: EventBus, private queryBus: QueryBus) {} @Get() async bid(): Promise<object> { // We are hard-coding values here // instead of collecting them from a request this.eventBus.publish( new BidEvent('4ccd1088-b5da-44e2-baa0-ee4e0a58659d', '0ac04f2a-4866-42de-9387-cf392f64cd52', 233), ); return { status: 'PENDING', }; } @Get('/audiences') async getAudiences() { const allAudiences = await this.queryBus.execute(new GetAuctionQuery()); return allAudiences; } }
The real power of our system lies in BidSaga class. Responsibility of this Class (service) is to listen on BidEvents and to dispatch commands. Developers with experience with rxjs and writing effects in ngrx package will find this code familiar and easy to read.
@Injectable() export class BidSaga { @Saga() createBid = (events$: Observable<any>): Observable<ICommand> => { return events$.pipe( ofType(BidEvent), map((event: BidEvent) => { return new BidCommand(event.bidUser, event.auctionID, event.bidAmount); }), ); } @Saga() createBidSuccess = (events$: Observable<any>): Observable<ICommand> => { return events$.pipe( ofType(BidEventSuccess), flatMap((event: BidEventSuccess) => { return [ new MailCommand(event.user.email, { title: 'You did it...', message: 'Congrats', }), new PostponeAuctionCommand(event.auctionID), // create activity command ]; }), ); } }
Notice that we created bidTransactionGUID variable and we passed it to BidEvent, that value is used to glue commands and events.
As you can see in the code above, BidEvent will dispatch BidCommand. Further on, in our code BidHandler (for BidCommand) will dispatch either BidEventSuccess or BidEventFail.
export class AuctionModel extends AggregateRoot { constructor(private readonly auction: IAuctionInterface) { super(); } postponeAuction() { // validation and etc. // postpone it, and return new auction object with postponed date const auction = { ...this.auction }; this.apply(new AuctionEventsPostponed(auction)); } bidOnAuction(userID: string, amount: number) { // validation and etc. try { // business logic // upon successful bidding, dispatch new event this.apply(new BidEventSuccess(this.auction.id, amount, { email: '[email protected]', id: userID })); } catch (e) { // dispatch bid event fail action this.apply(new BidEventFail(e)); } } }
The model shown above is run through BidHandler service.
After BidEventSuccess is dispatched, new commands will be launched– MailCommand and PostponeAuctionCommand.
@Injectable() export class AuctionSaga { @Saga() createBid = (events$: Observable<any>): Observable<ICommand> => { return events$.pipe( ofType(AuctionEventsPostponed), flatMap((event: AuctionEventsPostponed) => { // send emails to all existing bidders const bidders = [ new MailCommand('bidder1@emailid', { title: 'Someone made a bid', message: 'Hurry up', }), new MailCommand('bidder2@emailid', { title: 'Someone made a bid', message: 'Hurry up', }), ]; return [ ...bidders, // create activity ]; }), ); } }
As we can see in the examples from above, everything is about dispatching commands and chaining them with new events. A new feature will mean the creation of new command and new events that are triggered after.
If anything fails through this process, we can dispatch cleaning command with bidTransactionGUID information to delete things associated with this bid in the system.
If it is applied to the right place and for the right scenario, the event-driven programming paradigm can be a huge win for application architecture. If you think of an application where the flow of the program is determined by events, it can be a perfect fit for this programming approach.
Repository: https://github.com/vladotesanovic/cqrs
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowHandle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
2 Replies to "How to use event-driven programming in Node.js"
I am getting error on running the nestjs application. Nest can’t resolve dependencies of the AppController (AppService, ?). Please make sure that the argument CommandBus at index [1] is available in the AppModule context.
Any idea as to why?
Hi @Arnab
Are you sure your provided all services in App.module ?
https://github.com/vladotesanovic/cqrs/blob/master/src/app.module.ts#L18