Introduction
Philosophy
In recent years, thanks to the advent of JavaScript server-side runtimes like Node.js, Deno and Bun, JavaScript - and especially TypeScript - have become popular languages of choice for backend development (in fact, TypeScript recently became the most used programming language on GitHub). Many popular frameworks have emerged in the ecosystem to facilitate backend development, such as Express and NestJS. These excel at handling routing, middleware and request/response life-cycles. However, while they have excellent abstractions for application and infrastructure concerns, they provide limited support for managing complex domain logic. Developers working in non-trivial domains wishing to leverage domain-driven design (DDD) and microservice development often find that these frameworks have significant shortcomings when it comes to implementing many well-known patterns, such as async inter-process communication (IPC) through messaging, event-driven architecture, CQRS and event sourcing. Depending on the use-case, this may be beyond the scope of what these frameworks offer and requires significant work to implement correctly.
DugongJS is a domain framework centered around the event sourcing pattern. With it, developers can implement DDD and microservice patterns effectively with ergonomic APIs. Its built using the ports-and-adapters (hexagonal) architecture, making it framework- and infrastructure-agnostic. It does not aim to replace the frameworks and tools you already use, but rather to integrate with them.
Domain-Driven Design (DDD)
DDD is a well-established and very powerful methodology for building complex software systems, emphasizing the importance of modeling the domain accurately and aligning the software design with real-world business processes. It is largely divided into two main areas: strategic design, which focuses on high-level architecture and the relationships between different parts of the system, and tactical design, which deals with lower-level details such as contextual domain models, aggregates, entities, value objects, and domain events. Both tactical and strategic DDD are essential when building complex systems, especially in a microservices architecture, where different services typically map to different bounded contexts.
DugongJS assumes familiarity with DDD principles and patterns in order to be used effectively. It provides abstractions and tools to help developers succeed with DDD, focusing on tactical patterns such as aggregates, entities, value objects, and domain events, as well as some support for strategic patterns like bounded contexts and context mapping.
Microservices
The microservice architecture has gained significant popularity in recent years as a way to build scalable and maintainable software systems. Microservices provide both organizational and technical benefits: in DDD terms, they align well with the strategic design principle of bounded contexts, allowing different parts of the system to be worked on independently and to define their own ubiquitous languages and domain models. Technically, microservices enable independent deployment, scaling, and technology choices for each service. In general, the primary motivation to adopt microservices can be attributed to these two main factors. When the organizational factor is essential - that is, when the goal is to create software that embodies rich domain models that focus on behavior and real-world business processes rather than just data and CRUD operations - popular frameworks often do not provide sufficient functionality.
Take NestJS, one of the most popular frameworks for building backend systems with TypeScript, as an example. It provides a microservices package that enables services to communicate over various transport layer protocols other than HTTP (e.g., TCP, Redis, NATS, MQTT, Kafka, gRPC) and developers may have some success decomposing monolithic applications into microservices using this package. The main benefit of the package is to allow the underlying infrastructure and communication mechanisms to be abstracted away. In particular, it allows in-process communication (function calls) to be replaced with inter-process communication (network requests) without significant changes to the application code. However, it's important to realize that this only focuses on the purely technical aspect of microservice architecture and does not address the organizational or domain model side. Implementing microservices does indeed come with several technical challenges, but the main challenges often lie in the domain modeling and design of the services themselves.
DugongJS is designed to facilitate microservice development, providing several tools to help developers implement microservice patterns effectively, such as async IPC through messaging tied to domain events, CQRS and event sourcing. It can be combined with NestJS or other frameworks to add the missing capabilities mentioned above.
Event Sourcing
Backend services traditionally persist data in relational databases, where each resource is stored as a row in a table. When resources are created, updated or deleted, the corresponding row is then inserted, modified or removed. This approach is straightforward and clearly works well for many use cases. However, it also comes with several drawbacks, especially when coupled with DDD and microservice development. Common challenges include object-relational impedance mismatch, lack of aggregate versioning, and the dual-write problem, to name a few.
In DDD, a major emphasis is placed on identifying and modeling the the relevant events in the domain and use these domain events as the foundation for the system's behavior. A domain event represents a significant occurrence in the domain that reflects a change in state of an entity or aggregate. The event sourcing pattern is a powerful technique for implementing aggregates, where every state change of the aggregate is persisted as a domain event. The aggregate is structured around the creation and consumption of these domain events. When the current state (or a historical state) needs to be obtained, the persisted domain events are read into memory and applied to an instance of the aggregate class.
Command Query Responsibility Segregation (CQRS)
One of the main drawbacks of event sourcing is that an event log is not an effective basis for queries. If a service needs to query aggregates based on a set of given criteria, as is typically needed in most applications, the event log does not directly provide a means of doing that. It is unable to provide the current state of the aggregate, just the events that lead to the current state. Command query responsibility segregation (CQRS) is a pattern that addresses this issue by separating write operations (commands) from read operations (queries). Using CQRS, commands and queries are handled by separate processes. Commands are issued against event-sourced aggregates that generate domain events. A separate query side reads these domain events and generates its own query-optimized data store. Queries are then carried out against this other data store.
While this solves the query problem, it also introduces the problem of eventual consistency. Since the query side is updated asynchronously based on the events generated by the command side, there is an arbitrary time lag between when a command is processed and when the corresponding data is available for querying. This typically offsets some of the complexity to client applications, because they cannot immediately query data after issuing a command.
DugongJS makes it straightforward to implement CQRS, by setting up message consumers that read domain events emitted by the command side to generate query models.