← All projects

Modular Monolith .NET Template

A starter template for modular monoliths: each module is a self-contained Clean Architecture slice with its own DbContext and schema, composed by a thin Web API host.

Open sourceAuthor2026
.NET 10ASP.NET CoreEF CorePostgreSQLClean ArchitectureModular MonolithJWTTestcontainersDockerGitHub Actions
View source on GitHub

Most .NET projects start as a single API and slowly turn into a big ball of mud: every feature reaches into every table, and splitting anything out later means a rewrite. Microservices fix the boundaries but bring network calls, distributed transactions and a lot of operations work before the product has even found its users.

This template sits in between. It is one deployable, but every business module is a self-contained Clean Architecture slice with its own database schema, so the boundaries stay real. It grew out of what I learned building a modular monolith HR system in production.

How it is structured

src/
  BuildingBlocks/        shared kernel, no business logic
  Modules/
    Users/
      Users.Domain/          aggregate + repository interfaces
      Users.Application/     use cases, DTOs
      Users.Infrastructure/  DbContext (schema "users"), JWT, migrations
  Host/WebApi/           composition root
tests/
  Users.UnitTests/  Architecture.Tests/  WebApi.IntegrationTests/

Adding a module is one line in the host:

builder.Services.AddUsersModule(builder.Configuration);
builder.Services.AddPayrollModule(builder.Configuration);

The host never sees a module's internals, only its Add{Module}Module() entry point and its application services.

Rules that keep the boundaries honest

  • Schema per module. Each module owns a DbContext, a schema and its own migrations history table. No module queries another module's tables; it calls that module's application service instead.
  • Dependencies point inward. Domain has no reference to EF Core or ASP.NET Core; Application does not know Infrastructure exists.
  • Enforced by tests, not by code review. Architecture tests built with NetArchTest fail CI the moment someone adds a forbidden reference:
Types.InAssembly(UsersDomain)
    .ShouldNot()
    .HaveDependencyOnAny("Users.Infrastructure", "Microsoft.EntityFrameworkCore")
    .GetResult();

Production basics included

  • One error contract. Application exceptions are mapped to RFC 7807 ProblemDetails in a single IExceptionHandler, so controllers contain no try/catch and clients always get the same shape.
  • Fail-fast configuration. JWT options are validated on startup. A missing or short signing key stops the app instead of breaking the first login in production.
  • Rate-limited auth endpoints with the built-in ASP.NET Core rate limiter.
  • Health checks at /health, including a database check.
  • Docker multi-stage image running as a non-root user, plus Docker Compose with PostgreSQL.
  • Central Package Management, so every package version lives in one file.

Testing

  • Unit tests cover the auth use cases and domain invariants with Moq.
  • Architecture tests guard the module boundaries.
  • Integration tests boot the real API with WebApplicationFactory against PostgreSQL in Testcontainers and apply the real migrations, so the tests exercise the same SQL that runs in production.

GitHub Actions builds with warnings as errors, checks formatting, runs every test suite and builds the Docker image on each push.

Trade-offs I chose deliberately

  • Application services instead of MediatR. It keeps the template small and readable. CQRS can be added inside one module's Application layer without touching the others.
  • Synchronous in-process calls between modules. Simple and transactional for now. The next step is integration events with an outbox when modules need to be decoupled further.

© 2026. All rights reserved.