.NET 10Modular MonolithFree Project Template

A Modular Monolith that keeps its boundaries

Four isolated modules, public contracts between them, in-process events and one PostgreSQL schema each. Architecture tests fail the build the moment a module reaches into another module's internals. JWT auth, EF Core, OpenTelemetry, Aspire, Docker and 100+ tests are already wired.

Get the template
No Credit Card Required • 100% Free
By requesting the template, you agree to receive emails with .NET newsletter from antondevtips. You can unsubscribe at any time.
7,000+developers use this template
shipping-monolithpostgres
  • users.*
  • shipments.*
  • carriers.*
  • stocks.*

Owns the shipment lifecycle. Calls Stocks and Carriers only through their public contracts.

contractsIStockModuleApiICarrierModuleApi
Modules
4

Modules

Projects
25

Projects

Tests
118

Tests

Endpoints
20

Endpoints

Framework
.NET 10

Framework

Inside a module

One use case, five files you can read

Every feature is a vertical slice inside its module. Open the folder and the whole use case is in front of you, from the route to the event it raises.

Shipments/Features/CreateShipment/CreateShipment.Endpoint.cs
public class CreateShipmentApiEndpoint : IApiEndpoint { public void MapEndpoint(WebApplication app) { app.MapPost(RouteConsts.BaseRoute, Handle); } private static async Task<IResult> Handle( [FromBody] CreateShipmentRequest request, IValidator<CreateShipmentRequest> validator, ICreateShipmentHandler handler, CancellationToken cancellationToken) { var validationResult = await validator.ValidateAsync(request, cancellationToken); if (!validationResult.IsValid) { return Results.ValidationProblem(validationResult.ToDictionary()); } var response = await handler.HandleAsync(request, cancellationToken); if (response.IsError) { return response.Errors.ToProblem(); } return Results.Ok(response.Value); } }

The route

  • A Minimal API endpoint that implements IApiEndpoint and is discovered by assembly scanning at startup.
  • Validation runs first and returns a ValidationProblem, so the handler never sees a bad request.
  • Errors become ProblemDetails through ToProblem(). No exception is thrown for business failures.
  • No controllers, no [FromServices], no MediatR sitting between the route and the logic.
Boundaries

How modules talk, and how they can't

A modular monolith only stays modular if something stops the shortcuts. Here are the three things that connect modules, and the test that fails when anything else does.

01

Public contracts

A module that others need exposes one interface in its own PublicApi project. Internals stay invisible to every caller.

IStockModuleApiICarrierModuleApi
02

In-process events

Side effects travel as events instead of direct calls. Creating a shipment raises one event and two handlers pick it up.

IEventIEventPublisherIEventHandler<T>
03

A schema per module

Each module owns its DbContext, its PostgreSQL schema and its own migration history table. No shared tables, no cross-module joins.

users.*shipments.*carriers.*stocks.*
Common/Modules.Common.Tests.Architecture/ModuleTests.cs
[Fact] public void ShipmentsModule_ShouldOnlyReference_PublicApiProjects() { var shipmentsAssemblies = GetShipmentsModuleAssemblies(); var stocksInternalResult = Types.InAssemblies(shipmentsAssemblies) .Should() .NotHaveDependencyOnAny( StocksDomainNamespace, StocksInfraNamespace, StocksFeaturesNamespace) .GetResult(); Assert.True(stocksInternalResult.IsSuccessful, $"Shipments module should not reference internal Stocks projects: " + string.Join(", ", stocksInternalResult.FailingTypeNames ?? [])); }
Passed! - Failed: 0, Passed: 118, Skipped: 0

This is the part most modular monoliths skip. Five NetArchTest rules run with the rest of the suite: every module is checked for dependencies on other modules, and Shipments is checked specifically for reaching past a PublicApi project. The day someone adds the wrong project reference, the build tells them, not a code reviewer six weeks later.

Included

The boring parts, already decided

Everything a new .NET service needs before it can do anything useful, configured and running.

Minimal APIs, no MediatR

Endpoints, handlers and validators are found by assembly scanning. Handlers are plain classes behind a small interface, so there is no dispatcher to debug.

EF Core and PostgreSQL

A DbContext per module, migrations, entity configurations and an auditing interceptor. No repository ceremony.

Auth that survives production

ASP.NET Identity with JWT, refresh tokens and revocation, plus policy-based authorization defined per module.

Result pattern and validation

A Result type with typed errors, FluentValidation per use case and a global exception handler returning ProblemDetails.

Serilog, Seq and OpenTelemetry

Structured logs land in Seq, traces and metrics in Jaeger. Every module has its own activity source and tracing middleware.

Aspire and Docker Compose

Run it with .NET Aspire for local development or bring the whole stack up with Docker Compose. Both paths are configured.

Tests you can run today

100+ tests: unit tests, integration tests on Testcontainers with Respawn cleanup, and NetArchTest rules for module isolation.

Quality gates in the build

Meziantou, Sonar and Roslynator analyzers with warnings as errors, central package management and .editorconfig.

Swagger and 21 ready requests

Swagger UI in Development plus 21 .http files with an environment file, so you can call every endpoint from your IDE.

The shape of it

Four modules, one deployment, one database

One .NET application in Docker, four modules that never touch each other's tables, and one PostgreSQL instance holding a schema per module.

Modular monolith architecture: Users, Shipments, Stocks and Carriers modules inside one .NET application in Docker, each writing to its own PostgreSQL schema
Seq collects the structured logs, Jaeger collects the traces, and every module keeps its own schema.
The solution

A folder layout that explains itself

Modules on the outside, shared plumbing in Common, one host that composes them. You can point at any folder and say what it owns.

  • Each module keeps its Domain, Infrastructure and Features projects, and registers itself with one AddXModule call.
  • Only Carriers and Stocks ship a PublicApi project, because only they are called by another module.
  • Common holds the Result type, the event publisher, endpoint discovery and the global exception handler.
  • Adding a module means copying a folder, adding four project references and one registration line.
  • Outgrown the monolith? A module already has its own contract, schema and migrations, so extracting it is a small move, not a rewrite.
modular-monolith-template/src/
Shipments/ # one folder per module
Modules.Shipments.Domain/ # entities, value objects
Modules.Shipments.Infrastructure/ # DbContext, migrations
Modules.Shipments.Features/ # vertical slices
Modules.Shipments.Tests.Unit/
Modules.Shipments.Tests.Integration/ # Testcontainers
Stocks/
Modules.Stocks.PublicApi/ # the only entry point
Carriers/
Users/
Common/ # Result, events, error handling
Modules.Common.Tests.Architecture/ # boundary rules
ModularMonolith.Host/ # Program.cs, seeding, .http files
ModularMonolithTemplate.AppHost/ # .NET Aspire
docker-compose.yml # api, postgres, seq, jaeger

Free Project Template, .NET 10.

Clone it, run it, and start on your own domain instead of your own plumbing.

Get the template
Getting started

Running before your coffee gets cold

Three steps from the email in your inbox to a request hitting a real database.

  1. 1

    Drop your email

    The template arrives in 2-3 minutes as ready-to-run solution.

  2. 2

    Start the stack

    Aspire provisions PostgreSQL for you, or Docker Compose brings up the API, PostgreSQL, Seq and Jaeger together.

    docker compose up -d
  3. 3

    Add your own feature

    Copy a feature folder and rename it. Add your code. Run the solution. Swagger is on localhost:5000, Seq on 8081, Jaeger on 16686. Open a .http file, fire a request and you're ready.

Two templates

Which one should you start from?

Both templates build the same shipping application with the same stack. They differ in how hard the boundaries are.

Projects

Modular Monolith
25 projects, three per module
.NET Backend
14 projects, one Core

Boundaries

Modular Monolith
Separate projects plus PublicApi contracts
.NET Backend
Namespaces plus layer architecture tests

Cross-domain calls

Modular Monolith
Contract interfaces in dedicated PublicApi projects
.NET Backend
Contract interfaces with tracing decorators inside Core

Best for

Modular Monolith
Teams that may extract a module into a service later
.NET Backend
Teams that want the smallest solution to navigate

Stack

Modular Monolith
.NET 10, EF Core, Postgres, JWT, OTel, Aspire
.NET Backend
.NET 10, EF Core, Postgres, JWT, OTel, Aspire

Want the same stack with fewer projects and every feature in one folder? Start from the Vertical Slice template instead.

See the .NET Backend template
Questions

Questions people ask

Is a modular monolith better than microservices?

It is better for most teams starting out. You get the module boundaries, the separate schemas and the contracts, without the network calls, the distributed transactions and the deployment pipeline that microservices demand. When one module genuinely needs to scale on its own, it already has a contract and its own data, so extracting it is a small move rather than a rewrite.

Which .NET version does the template target?

.NET 10. The patterns work on .NET 8 and 9 as well, though you will need to change the target framework and step back a few package versions.

Why is there no MediatR?

Because a handler is just a class. The template registers handlers by assembly scanning and the endpoint injects the interface it needs, so a stack trace goes straight from the route to your code. There is no pipeline to configure, no behaviour ordering to reason about and one less licensed dependency.

Why are there no repositories?

DbContext is already a unit of work and DbSet is already a repository. Wrapping them again costs you LINQ, change tracking and half of EF Core's query capability in exchange for an abstraction most teams never swap out. Integration tests run against a real PostgreSQL container instead.

Why are there no service classes?

A ShipmentService starts with one method and ends up with fifteen, and every caller drags in all of them. Here each use case is its own handler class: the Shipments domain has eight slices, from CreateShipment to CancelShipment. The constructor injects only what that one use case needs. If you need to write unit tests, you will need only 2 mocked dependencies instead of 10.

How do modules communicate?

Two ways. A synchronous call goes through the target module's public contract interface, which lives in its own PublicApi project. Anything that can happen after the fact travels as an in-process domain event, so the module raising it does not know who reacts.

How do I add my own module?

Copy one of the existing module folders, rename the projects, add its registration call in the host and give it a PostgreSQL schema. If other modules need to call it, add a PublicApi project with one interface. The architecture tests will tell you if you wire anything the wrong way.

Is it really free?

Yes, no credit card required. You will also get my .NET newsletter, and you can unsubscribe whenever you want.

Should I use this or the Vertical Slice template?

Pick this one if you want hard boundaries enforced by separate projects and expect to extract a service one day. Pick the .NET Backend template if you want the same stack in fourteen projects with every feature in one folder.

Start on your domain, not your plumbing

The full Modular Monolith template, free, in your inbox. Clone it, run it, and spend day one on the part that is actually yours.

Free Project Template. No credit card.