Skip to content

Commit 3612a70

Browse files
author
Mateusz Gajda
committed
update docs
1 parent f1e25d3 commit 3612a70

10 files changed

+147
-10
lines changed

README.md

+109-8
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,127 @@
33
The project was created to demonstrate how we can create a modular monolith in Node.js. The main idea of the project was high separation of each module from each other. This allows each module to be developed independently by different teams. As the project develops this will also allow us to easily extract single modules into microservices. For this purpose, I chose a fairly well-known domain and on its example tried to present how the implementation of such an application can look like.
44
In this file, as well as in the ADR, you will find implementation details as well as decisions that were made during this project. I would like every important point to be documented so that you can easily see the reasoning behind a particular choice.
55

6+
# Table of contents
7+
8+
- [Travelhoop - Modular monolith in Node.js](#travelhoop---modular-monolith-in-nodejs)
9+
- [Table of contents](#table-of-contents)
10+
- [1. Domain](#1-domain)
11+
- [1.1 About domain](#11-about-domain)
12+
- [1.2 Event storming](#12-event-storming)
13+
- [1.2.1 User module](#121-user-module)
14+
- [1.2.2 Host module](#122-host-module)
15+
- [1.2.3 Booking module](#123-booking-module)
16+
- [1.2.4 Review module](#124-review-module)
17+
- [2. Architecture](#2-architecture)
18+
- [2.1 Overview](#21-overview)
19+
- [2.1.1 Event driven architecture](#211-event-driven-architecture)
20+
- [2.1.2 Vertical slices](#212-vertical-slices)
21+
- [2.2 Modules](#22-modules)
22+
- [2.2.1 AppModule interface](#221-appmodule-interface)
23+
- [2.2.2 Module loader](#222-module-loader)
24+
- [2.3 Communication](#23-communication)
25+
- [2.3.1 Message broker](#231-message-broker)
26+
- [2.4 Architectural Decision Records](#24-architectural-decision-records)
27+
628
# 1. Domain
729

8-
## About domain
30+
## 1.1 About domain
931
Travelhoop - It is an application thanks to which you can offer free accommodation to travelers. It allows you to make new acquaintances, friendships and meet people from all over the world. Each user can search for accommodation in any location in the world. This is a replication of the fairly well-known Couchsurfing app.
1032

11-
## Event storming
33+
## 1.2 Event storming
1234
To discover the domain and what was behind it, I decided to use a very popular method called [event storming](https://www.eventstorming.com/). Below I present a diagram which was created after one session, on the basis of which the application will be built.
1335

1436
![Event storming overview](./docs/images/es-overview.jpg "Event storming overview")
1537

16-
### User module
38+
### 1.2.1 User module
1739
![User module](./docs/images/es-user-module.jpg "User module")
18-
### Property module
19-
![Property module](./docs/images/es-property-module.jpg "Property module")
20-
### Booking module
40+
### 1.2.2 Host module
41+
![Host module](./docs/images/es-host-module.jpg "Host module")
42+
### 1.2.3 Booking module
2143
![Booking module](./docs/images/es-booking-module.jpg "Booking module")
22-
### Review module
44+
### 1.2.4 Review module
2345
![Review module](./docs/images/es-review-module.jpg "Review module")
2446

2547
# 2. Architecture
2648

27-
## Architectural Decision Records
49+
## 2.1 Overview
50+
51+
### 2.1.1 Event driven architecture
52+
To keep the independence of the modules high, I decided to use an event driven approach. It will allow us to synchronize the modules with each other asynchronously. It will also allow for low coupling between them. We will exchange messages (events) between modules. They will carry information about changes that occurred in one of the modules, so that the rest of the modules can react in an appropriate way, updating their state or performing some other operation.
53+
54+
### 2.1.2 Vertical slices
55+
TBU
56+
## 2.2 Modules
57+
58+
### 2.2.1 AppModule interface
59+
Each module expose implementation of the following interface:
60+
61+
```ts
62+
export interface UseDependencies {
63+
dbConnection: DbConnection;
64+
redis: Redis;
65+
}
66+
export interface AppModule {
67+
basePath: string;
68+
name: string;
69+
path: string;
70+
use: (app: Application, deps: UseDependencies) => void;
71+
dispatchEvent(event: Event): Promise<void>;
72+
}
73+
```
74+
75+
Here we define:
76+
- **basePath** - for a routing purposes we need to define main route for each module. It will help us orginise the module under specific paths
77+
- **name** - the name of the module. By the implementation of module loader, this should be the name like *some-module*
78+
- **use(app: Application, deps: UseDependencies)** - it's a method which we later use in our `app.ts` file to load whole module. Here we are passing common dependencies like connection to databases, which are shared across all modules. We can also add here our custom error handling, specific for each module, or some other middlewares. What is important, all middlewares defined here, will be a part of the whole pipeline of our express application. So be careful.
79+
- **dispatchEvent(event: Event)** - it's a method, which will be later used by our background message dispatcher, to pass integration events from e.g. queue to our module.
80+
81+
### 2.2.2 Module loader
82+
In `server.ts` which is a main file of our application we are using module loader to load all the modules and register it into container.
83+
Next, registered modules are injected into app.ts, where we define an express execution pipeline. There we are registering all the modules, by executing their `use` method:
84+
85+
```ts
86+
export const createApp = ({ errorHandler, modules, dbConnection, redis }: AppDependencies): Application => {
87+
(...)
88+
89+
modules.forEach(m => m.use(app, { dbConnection, redis }));
90+
91+
(...)
92+
```
93+
94+
What is important, to correctly register module in our application we need to define a dependency to this module in `package.json`. Also the name of the module folder must follow this convention *some-module*. E.g. for user module, the folder name must be *user-module*. Under the hood we are loading modules interface by looking up for specific directories in our `/node_modules/@travelhoop` folder.
95+
96+
## 2.3 Communication
97+
98+
### 2.3.1 Message broker
99+
100+
This is the main unit responsible for asynchronous message exchange between modules. It issues the `publish` method responsible for publishing them. Internally, the message broker can implement different types of `MessageDispatcher`.
101+
102+
```ts
103+
interface MessageBrokerDependencies {
104+
messageDispatcher: MessageDispatcher;
105+
}
106+
107+
export class MessageBroker {
108+
constructor(private readonly deps: MessageBrokerDependencies) {}
109+
110+
async publish<TMessage extends object>(message: TMessage) {
111+
await this.deps.messageDispatcher.publish(JSON.stringify(message));
112+
}
113+
}
114+
```
115+
116+
Message dispatcher is a simply interface, implemented in various of way.
117+
118+
```ts
119+
export interface MessageDispatcher {
120+
publish(message: string): Promise<void>;
121+
}
122+
```
123+
124+
One of the implementaton is `RedisMessageDispatcher`. Under the hood it use a Redis queue to push a message to the consumers.
125+
Since our application is a single deployment unit, `MessageBroker` publishes messages to a single, common queue for all modules. The message is then retrieved by a single consumer. The implementation of the consumer can be found here `/modular-monolith/src/shared/infrastructure/src/messaging/background.message-dispatcher.ts`. It listen for a new message on queue and dispatch it to all the modules, by their interface.
126+
Responsibility of each module is to decide if the message should be handled or not. Our main application doesn't care about it. Our modules are responsible for themselve.
127+
128+
## 2.4 Architectural Decision Records
28129
All architectural decisions are keeped in `./docs/adr` directory. It captures an important architectural decision made along with its context and consequences. To automate this process I've used [adr-tools](https://github.com/npryce/adr-tools) library. It should help you understand, why I've made some decisions in this project. Very often we have a couple of possibility how to solve some problems. Often it has some pros and cons. It is important to make this decisions, knowing potential consequences.

docs/adr/0003-di-container-for-each-module.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 7. DI container for each module
1+
# 3. DI container for each module
22

33
Date: 2021-04-01
44

docs/adr/0004-share-infrastructure-between-all-modules.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 8. Share infrastrcture between all modules
1+
# 4. Share infrastrcture between all modules
22

33
Date: 2021-04-01
44

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# 5. Use local contract for integration events
2+
3+
Date: 2021-04-10
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
Each of the module expose integration events. Other modules can listen for these events and execute specific action. We need to define events somewhere, to be able to properly handle it in each module.
12+
13+
## Possible solutions
14+
15+
1. Shared contracts.
16+
Pros:
17+
- less code duplication
18+
- going into microservices it's easy to copy this directory, with all the codebase
19+
- easier to just import needed things instead of defining it multiple times in every module
20+
Cons:
21+
- coupling to mutiple modules
22+
23+
1. Local contracts
24+
Pros:
25+
- less coupling
26+
Cons:
27+
- more effort to maintain integration between events e.g. contract testing
28+
- a lot of duplicated code
29+
30+
## Decision
31+
32+
One of the goals of this project is to keep each module as independent as possible, I decided to use local contracts
33+
34+
## Consequences
35+
36+
To maintain contract consistency, contract testing will need to be in place.

docs/images/es-booking-module.jpg

-501 Bytes
Loading

docs/images/es-host-module.jpg

194 KB
Loading

docs/images/es-overview.jpg

665 Bytes
Loading

docs/images/es-property-module.jpg

-201 KB
Binary file not shown.

docs/images/es-review-module.jpg

19.5 KB
Loading

docs/images/es-user-module.jpg

-5.35 KB
Loading

0 commit comments

Comments
 (0)