You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This project contains a set of templates to build an application following specific microservice architecture guidelines that have evolved over the years
3
+
This project contains a set of templates to scaffold a small message-driven system: a **Web** frontend, an **API**, a **Worker**, and a **Shared** project for contracts.
4
+
5
+
In this repo, **MSA** stands for **Message-driven Service Architecture**: services communicate primarily through commands and events over a message broker, which keeps them loosely coupled and enables independent scaling and deployment.
6
+
7
+
If you’re new here, start with [Quick start](#quick-start), then read [What you get](#what-you-get). The service-specific sections below explain the responsibilities and the reasoning behind the split.
> The templates currently use **MassTransit**, but this will be removed in a future update and replaced by my own library **Conveyo**: https://github.com/ftechmax/conveyo
15
+
> MassTransit’s licensing model changed, and I want the messaging stack to remain fully open and free.
-[Message-driven Service Architecture Worker](#message-driven-service-architecture-worker)
26
+
-[Message-driven Service Architecture API](#message-driven-service-architecture-api)
27
+
-[Message-driven Service Architecture Web](#message-driven-service-architecture-web)
18
28
-[How to contribute](#how-to-contribute)
19
29
30
+
## Why this exists
31
+
32
+
I’ve been building message-driven systems for ~15 years. Over time you end up rediscovering the same handful of patterns: how services talk to each other, how you model commands and events, how you make failures visible, how you deploy safely, and how you keep a system operable once it’s running 24/7.
33
+
34
+
These templates are the accumulation of those lessons, packaged as a starting point that gets the boring, but critical, parts right:
35
+
36
+
- A clear split between the **HTTP edge** in the API and **asynchronous domain work** in the worker
37
+
- A default stack for **messaging, persistence, caching, and telemetry** that works well in practice
38
+
-**Kubernetes** manifests are included so you can deploy quickly on local, self-hosted, or cloud environments
39
+
-**Opinionated but flexible** architecture that encourages best practices without being restrictive
40
+
41
+
This is not meant to be the only way to do things. It’s just a set of defaults that has proven itself in real projects, real incidents, and real deployments.
42
+
20
43
## Template Installation
21
44
22
45
Install the latest version of the `MSA.Templates` package:
@@ -27,7 +50,7 @@ dotnet new install MSA.Templates
27
50
28
51
## Quick start
29
52
30
-
using the script
53
+
Using the script
31
54
32
55
```console
33
56
.\generator.ps1 `
@@ -39,7 +62,7 @@ using the script
39
62
-DestinationFolder c:/git
40
63
```
41
64
42
-
or manually
65
+
Or manually
43
66
44
67
```console
45
68
mkdir -p c:/git/awesome/src
@@ -51,69 +74,113 @@ dotnet new msa-api -n Awesome -o api
51
74
dotnet new msa-web -n Awesome -o web
52
75
```
53
76
54
-
## Microservice Architecture Worker
77
+
## What you get
78
+
79
+
These templates are intended to be used together as a small “vertical slice” of a message-driven system:
80
+
81
+
-**Shared**: contracts and shared abstractions used across services within the same domain.
82
+
-**Worker**: consumes commands and external events, applies business logic, persists state, and publishes domain events.
83
+
-**API**: serves HTTP endpoints, validates input, sends commands to the worker, and fans out updates through SignalR.
84
+
-**Web**: a blank Angular Next frontend scaffolded to work with the API.
85
+
86
+
The templates come with sensible defaults for:
87
+
88
+
-**Messaging** via MassTransit (Conveyo coming soon!) + RabbitMQ for commands and events
89
+
-**Persistence** via MongoDB
90
+
-**Caching** via Valkey/Redis using StackExchange.Redis
91
+
-**Observability** via OpenTelemetry for traces, metrics, and logs
92
+
93
+
## Message-driven Service Architecture Worker
94
+
95
+
The worker is the service that **consumes commands/events**, runs domain logic, persists state, and **publishes domain events**. It is preconfigured for Kubernetes and designed to be idempotent, observable, and resilient.
55
96
56
-
This creates a layered .net application specifically designed for handling domain events and is preconfigured to run in a Kubernetes environment.
97
+
For the full breakdown of the project structure and handler patterns, see [docs/worker.md](docs/worker.md).
57
98
58
99
```mermaid
59
100
graph LR;
60
101
Wb[Web]-->A[API];
61
102
A-->Wb;
62
-
A-->W;
63
-
W[Worker]-->A;
103
+
A-- sends commands -->W;
104
+
W[Worker]-- publishes events -->A;
64
105
style W fill:#555
65
106
```
66
107
108
+
In real systems you often end up with **multiple workers**, typically split by domain or bounded context. The important bit is that they stay independent: each worker consumes its own commands, owns its own state, and can react to events published by other workers.
109
+
110
+
Here’s a common “two workers” setup. The API sends commands onto the bus, and both workers publish events back. Workers can also subscribe to each other’s events without direct coupling.
111
+
112
+
```mermaid
113
+
graph LR;
114
+
Wb[Web]-->A[API];
115
+
A-->Wb;
116
+
117
+
A-- sends commands -->B((RabbitMQ));
118
+
B-- delivers commands -->W1[Worker A];
119
+
B-- delivers commands -->W2[Worker B];
120
+
121
+
W1-- publishes events -->B;
122
+
W2-- publishes events -->B;
123
+
B-- delivers events -->A;
124
+
125
+
style W1 fill:#555
126
+
style W2 fill:#555
127
+
```
128
+
67
129
To create a worker execute the following command, replacing `Awesome` with your domain name (e.g. `Accounting` or `Products`):
68
130
69
131
```console
70
132
dotnet new msa-worker -n Awesome -o worker
71
133
```
72
134
73
-
> :rocket: Check the wiki about [worker services](https://github.com/ftechmax/msa-templates/wiki/worker-service) for details about setting up and using this service type.
135
+
## Message-driven Service Architecture API
74
136
75
-
## Microservice Architecture API
137
+
The API is the **HTTP edge**: it validates input, serves reads from a local read model, and forwards writes as **commands to the worker** over the message bus. It also consumes events to invalidate caches and notify clients (SignalR).
76
138
77
-
This creates a layered .net 6 application specifically designed for handling API calls and generating domain commands and is preconfigured to run in a Kubernetes environment.
139
+
For the full breakdown of controllers, application services, and local event handling, see [docs/api.md](docs/api.md).
78
140
79
141
```mermaid
80
142
graph LR;
81
143
Wb[Web]-->A[API];
82
144
A-->Wb;
83
-
A-->W;
84
-
W[Worker]-->A;
145
+
A-- sends commands -->W;
146
+
W[Worker]-- publishes events -->A;
85
147
style A fill:#555
86
148
```
87
149
88
-
To create an api execute the following command, replacing `Awesome` with your domain name (e.g. `Accounting` or `Products`):
150
+
To create an API execute the following command, replacing `Awesome` with your domain name (e.g. `Accounting` or `Products`):
89
151
90
152
```console
91
153
dotnet new msa-api -n Awesome -o api
92
154
```
93
155
94
-
> :rocket: Check the wiki about [api services](https://github.com/ftechmax/msa-templates/wiki/api-service) for details about setting up and using this service type.
156
+
## Message-driven Service Architecture Web
95
157
96
-
## Microservice Architecture Web
158
+
This creates a simple Angular SPA hosted in an Nginx container and preconfigured to run in a Kubernetes environment. It uses [Transloco](https://ngneat.github.io/transloco/) to manage translations.
97
159
98
-
This creates a blank Angular Next application hosted in a nginx container and is preconfigured to run in a Kubernetes environment and uses [Transloco](https://ngneat.github.io/transloco/) to manage translations.
160
+
The web template is kept intentionally light, but it’s meant to fit the flow:
161
+
162
+
-**Calls the API** for queries and user-initiated actions.
163
+
-**Triggers commands** indirectly by hitting HTTP endpoints. The API turns those requests into messages.
164
+
-**React to server-side changes**: the architecture supports pushing updates through SignalR so your UI can subscribe to events and refresh relevant screens.
165
+
-**Deploys cleanly** as a static-ish frontend behind Nginx, which maps nicely to Kubernetes.
166
+
167
+
The goal is to give you a working starting point that matches the rest of the stack, without forcing a specific UI architecture on top.
99
168
100
169
```mermaid
101
170
graph LR;
102
171
Wb[Web]-->A[API];
103
172
A-->Wb;
104
-
A-->W;
105
-
W[Worker]-->A;
173
+
A-- sends commands -->W;
174
+
W[Worker]-- publishes events -->A;
106
175
style Wb fill:#555
107
176
```
108
177
109
-
To create an api execute the following command, replacing `Awesome` with your domain name (e.g. `Accounting` or `Products`):
178
+
To create a web app execute the following command, replacing `Awesome` with your domain name (e.g. `Accounting` or `Products`):
110
179
111
180
```console
112
181
dotnet new msa-web -n Awesome -o web
113
182
```
114
183
115
-
> :rocket: Check the wiki about [web services](https://github.com/ftechmax/msa-templates/wiki/web-service) for details about setting up and using this service type.
116
-
117
184
## How to contribute
118
185
119
186
Feel free to create a PR if you feel something is missing!
The API service is responsible for exposing the domain over HTTP. It is intentionally thin: it serves queries from a local read model and forwards commands to the worker over the message bus. This keeps business logic out of the HTTP layer while still providing a clean API surface.
4
+
5
+
## Responsibilities
6
+
7
+
The API is intentionally not your domain engine. Its main jobs are:
8
+
9
+
-**Own the HTTP contract**: controllers, DTOs, a versioning strategy when you need one, and input validation.
10
+
-**Validate early**: reject bad requests before they become messages that bounce around the system.
11
+
-**Send commands** to the worker via the bus using MassTransit. The API should not need direct DB access to do writes.
12
+
-**Serve reads efficiently**: the template uses projections and caches them in Valkey/Redis so read paths stay fast without coupling reads to the write model.
13
+
14
+
On the “reactive” side, the API also acts as a bridge back to clients. It consumes domain events published by the worker and uses them to:
15
+
16
+
-**Invalidate/refresh caches** so clients see fresh view models.
17
+
-**Notify connected clients** through SignalR so UIs can update without polling.
18
+
19
+
## Operational notes
20
+
21
+
Like the worker, it’s wired for observability via OpenTelemetry and comes with the usual HTTP service niceties such as health checks and Swagger in development.
22
+
23
+
## Project Structure
24
+
25
+
The API project is very simple in how it works. Its entry point is a set of HTTP endpoints (controllers). Each endpoint delegates to an application service. Queries are served locally from a read model, while commands are sent to the worker. The worker publishes events, which the API consumes to invalidate caches and notify connected clients, creating a clean loop.
26
+
27
+
### Controller
28
+
29
+
The `ExampleController.cs` file contains the HTTP endpoints for the `Example` domain. If you have more sub-domains, you should create additional controllers following the same pattern instead of growing a single controller indefinitely.
30
+
31
+
The controller usually follows these steps:
32
+
33
+
1.**Receive Request**: The controller accepts a request DTO from the client.
34
+
2.**Delegate to Application Service**: The controller forwards the call to the application service.
35
+
3.**Return Response**: For queries, the controller returns DTOs; for commands, it returns after the message is sent.
36
+
37
+
In this design, reads (GET endpoints) call local query methods, while writes (POST/PUT) forward commands to the worker.
38
+
39
+
### Application Service
40
+
41
+
The `ExampleService.cs` file contains the orchestration logic for the `Example` domain. It does not implement business rules; it coordinates reads and writes.
42
+
43
+
For queries, the application service typically follows these steps:
44
+
1.**Check Cache**: Try to return a cached response.
45
+
2.**Load Read Model**: Load documents from the read model when the cache is empty.
46
+
3.**Return Result**: Map and return DTOs, then store the result in cache.
47
+
48
+
For commands, the application service typically follows these steps:
49
+
1.**Receive Request**: Accept the request DTO from the controller.
50
+
2.**Create Command**: Convert the DTO into a domain command.
51
+
3.**Send Command**: Send the command to the worker over the bus.
52
+
53
+
### Local Event Handler
54
+
55
+
The API subscribes to domain/integration events from the bus (e.g. `ExampleCreatedEvent`, `ExampleUpdatedEvent`). This is handled by `LocalEventHandler.cs`.
56
+
57
+
The local event handler typically follows these steps:
58
+
1.**Receive Event**: The handler listens for events published by the worker.
59
+
2.**Invalidate Cache**: The handler removes affected cache entries so subsequent queries are consistent.
60
+
3.**Notify Clients**: The handler notifies connected clients (SignalR) that something changed.
61
+
62
+
#### Fault notifications
63
+
64
+
The handler also shows how to forward failures to clients by consuming `Fault<TCommand>` and publishing a `DomainFault` with a correlation id and trace id. Clients can use this information to correlate UI failures with logs and traces.
65
+
66
+
#### Using a Different Persistence Mechanism
67
+
68
+
Note that the default read model is MongoDB. This will be replaced by a Valkey read model in the future. However, you can replace it with any other database by implementing the repository interface accordingly.
The worker service is responsible for consuming commands and external events, applying business logic, persisting state, and publishing domain events. It typically runs as a background service and does not expose HTTP endpoints. The worker project template sets up a robust foundation for building message-driven services using MassTransit with RabbitMQ for messaging, MongoDB for persistence, and OpenTelemetry for observability.
4
+
5
+
## Responsibilities
6
+
7
+
In practice, the worker is where you put the “real work”:
8
+
9
+
-**Consumes commands** from the bus and treats them as the unit of work. Commands should be actionable and explicit, like `CreateOrder` instead of `OrderChanged`.
10
+
-**Handles external events** from other services and translates them into local commands when needed, so domain logic stays behind your own contracts.
11
+
-**Applies domain rules** and produces domain events as the outcome, meaning facts that happened, instead of leaking persistence concerns into handlers.
12
+
-**Persists state** (MongoDB by default) behind a repository abstraction.
13
+
-**Publishes events** back onto the bus so other services (and the API) can react asynchronously.
14
+
15
+
## Operational notes
16
+
17
+
-**Idempotent by design** because messages can be delivered more than once.
18
+
-**Observable**: traces, metrics, and logs are emitted via OpenTelemetry so you can answer “what happened to message X?”.
19
+
-**Resilient**: transient failures should be retryable and visible. MassTransit and RabbitMQ give you the building blocks, and you decide the policy.
20
+
-**Background jobs** belong here too. This template shows a hosted service for cache invalidation.
21
+
22
+
## Project Structure
23
+
24
+
The worker project is very simple in how it works. Because it is only allowed to communicate via messages, its entry point is a set of message handlers that respond to commands and events. Each handler delegates the actual business logic to a domain service, which encapsulates the core functionality of the service. The domain results in domain events, which are then published by the initial message handler, creating a clean loop.
25
+
26
+
### Command Handler
27
+
28
+
The `ExampleCommandHandler.cs` file contains the implementation of command handlers that process incoming commands. Each command handler is responsible for executing specific business logic when a command is received. This handler is designated to process `Example` domain commands. If you have more sub-domains, you should create additional command handlers following the same pattern instead of adding all command handlers to this single file.
29
+
30
+
The command handler usually follows these steps:
31
+
1.**Receive Command**: The handler listens for specific commands from the message bus.
32
+
2.**Pass Command to Domain Service**: The handler invokes the appropriate method on the domain service, passing along the entire command object.
33
+
3.**Convert Domain Events**: The domain service processes the command and returns a domain event object. The handler converts this domain event into an integration event suitable for publishing.
34
+
4.**Publish Integration Events**: After processing the command, the handler may publish domain events if the returned domain event object is not null.
35
+
36
+
### Domain Service
37
+
38
+
The `ExampleService.cs` file contains the core business logic for the `Example` domain. This service is responsible for processing commands delivered by its `CommandHandler` and generating domain events based on the business rules.
39
+
40
+
The domain service typically follows these steps:
41
+
1.**Receive Command**: The service receives the command object from the command handler.
42
+
2.**Create or Hydrate Aggregate**: The service either creates a new aggregate instance or retrieves an existing one from the repository, depending on the command.
43
+
3.**Apply Business Logic**: The service invokes methods on the aggregate to apply business rules and modify its state.
44
+
4.**Persist Changes**: The service saves the updated aggregate back to the repository.
45
+
5.**Return Domain Events**: The service returns any domain events that were generated as a result of processing the command.
46
+
47
+
#### Using a Different Persistence Mechanism
48
+
49
+
Note that the default persistence mechanism is MongoDB, but you can replace it with any other database by implementing the repository interface accordingly.
50
+
For event-sourcing scenarios, you can use my [MongoEventStore](https://github.com/ftechmax/mongo-eventstore) library as a starting point. In this case you would store the domain event object returned by the aggregate instead of persisting the aggregate state directly.
51
+
52
+
### External Event Handler
53
+
54
+
The `ExternalEventHandler.cs` file contains the implementation of event handlers that process incoming external events from other domains. Each event handler is responsible for executing specific business logic when an external event is received. This handler is designated to process external messages from the `Other.Worker.Contracts.Commands` namespace. An example would be handling a `UserCreatedEvent` from an identity service to create a corresponding local user profile. Or in our case we capture some remote code and link it to our `Example` aggregate.
0 commit comments