This is an Alarms management NestJS CRUD app, implemented using Hexagonal architecture (aka Ports & Adapters architecture), and CQRS architectural pattern with the help of @nestjs/cqrs.
-
the service' logic is divided and relocated into more specialized components: command handlers and query handlers.
Controller ---> Service ---> commandBus.execute(createAlarmCommand) -------------------------------------- || \=> calls CreateAlarmCommandHandler.execute //and queryBus.execute(new GetAlarmsQuery()) -------------------------------------- || \=> calls GetAlarmsQueryHandler.execute
-
the app emphasizes 2 different data models:
-
Domain-model: optimized for write intensive operations, and encapsulating the core business logic and behavior of the domain.
-
Read-model: optimized for read intensive operations such as: queries, reporting, and presenting data to users or external systems.
- [GOAL]
- improve query performance
- provide more efficient way to access data for specific use cases, e.g. aggregating, tailoring the data to fit the specific requirements of a read operation.
- [GOAL]
// /alarms/domain/alarm.ts export class Alarm { name: string; severity: AlarmSeverity; triggeredAt: Date; isAcknowledged = false; items = new Array<AlarmItem>() constructor(public id: string) {} acknowledge() { this.isAcknowledged = true; } addAlarmItem(item: AlarmItem) { this.items.push(item) } } export class AlarmItem { constructor( public id: string, public name: string, public type: string ) {} } // /alarms/domain/read-models/alarm.read-model.ts export class AlarmReadModel { id: string; name: string; severity: string; triggeredAt: Data; isAcknowledged: boolean; items: Array<{ name: string; type: string; }> }
-
-
the app uses two different data storage technologies, MongoDB; a NoSQL db; for read-side and Postgres DB for write-side.
// /alarms/infrastructure/persistence/orm/entities/alarm.entity.ts import { Entity, PrimaryColumn, Column, OneToMany } from "typeorm"; @Entity('alarms') export class AlarmEntity { @PrimaryColumn('uuid') id: string; ... @Column() triggeredAt: Date; @Column() isAcknowledged: boolean; @OneToMany(() => AlarmItemEntity, item => item.alarm, { cascade: true }) items: AlarmItemEntity[] } // /alarms/infrastructure/persistence/orm/schemas/materialized-alarm-view.schema.ts import { Prop, Schema, SchemaFactory, raw } from "@nestjs/mongoose"; @Schema() export class MaterializedAlarmView { @Prop({ unique: true, index: true }) id: string; @Prop() name: string; ... @Prop( raw([ { id: String, name: String, type: { type: String } } ]) ) items: Array<{ id: string, name: string, type: string }>; } export const MaterializedAlarmViewSchema = SchemaFactory.createForClass( MaterializedAlarmView )
- in contrast to the write-side, the denormalized view schema embeds the items attribute, which means the materialized alarm view document will contain all the informations right away, without the need to join the alarm items table which will help speed up read operations.
CQRS is a software architectural pattern that separates the concerns of reading data (queries) and writing data (commands) into separate models.
.[GOAL] to have different models and approaches for handling read and write operations in a system, rather than combining them into a single model.
---------> Command Model ---------> Write Database
||
|| Eventual
Client || consistency
||
\/
---------> Read Model ---------> Read Database
- The application data model is divided into 2 separate parts:
- Command model: handles write operations / data modification (handles the commands, and updates the state of the system)
- enforces business rules and validation logic to ensure that data changes are correct and consistent.
- Read/Query model: handles read operations / data retrieval (handles the events, and updates the read models)
- often involves denormalized data-structures or specialized views that cater to specific read/use cases.
- Command model: handles write operations / data modification (handles the commands, and updates the state of the system)
Eventual consistency is the outcome when the system prioritizes "Availability" and "Partition Tolerance" over immediate "Consistency", i.e., Nodes may have temporary inconsistencies; eventual consistency sacrifies consistency in the short-term, but achieves it in the long-term.
- [IN_THE_CONTEXT_OF] CQRS: eventual consistency is achieved by separating the read and write sides of the system
$ npm install
$ docker-compose up -d
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
Written with Love ❤️