A modular monolith reference architecture for .NET teams that want explicit boundaries today and a credible path toward service extraction later, without starting with microservice overhead.
ModularVerticalSlice.NET is both a reference architecture and a starter kit. It does not just show a folder structure. It encodes concrete architectural choices, their trade-offs, and the guardrails that keep those choices stable as the codebase grows.
It is aimed at teams that need:
- business boundaries enforced by the compiler and architecture tests
- message-driven coordination without introducing a broker on day one
- a modular monolith that can evolve without a disruptive rewrite
In concrete terms, that means safer refactors through executable boundary rules, durable messaging without introducing a separate broker up front, and a lower extraction cost because collaboration contracts stay explicit instead of leaking through a shared application core.
The sample domain is event ticket booking: Catalog owns events and ticket inventory, Bookings owns reservation and booking workflows, and Payments shows downstream processing boundaries.
This project starts from classic Vertical Slice Architecture: each use case owns its request path end to end instead of being split across horizontal technical layers.
It then adds two constraints that classic VSA often leaves implicit:
- explicit business-module boundaries under
Modules/ - explicit delivery boundaries under
Delivery/for consequence-handling capabilities that are application concerns but not business modules
It also keeps the useful parts of Clean Architecture: explicit boundaries, dependency discipline, testability, resilience, and a composition root. What it avoids is the extra ceremony that appears when repository layers, service layers, and abstractions are introduced before the codebase has earned them.
Entity behavior stays on the persisted entity by default. A separate Domain/ folder exists only when a rule, policy, or value object does not belong cleanly to one persisted entity, or when a cross-entity concept deserves its own home.
In practice this is a modular monolith with explicit boundaries, local transactions, and message-based coordination across modules.
This repository is a reference architecture and starter kit, not a reusable framework or NuGet product. Payments and downstream delivery flows are demonstration-grade application slices, useful for showing architecture and reliability patterns rather than claiming production-ready integrations out of the box.
Three common starting points:
- Classic monolith: fast to start, but boundaries are mostly implicit and extraction later means untangling accidental coupling.
- Microservice-first: explicit boundaries from day one, but also distributed transactions, eventual consistency, and operational complexity before the domain is stable enough to justify them.
- Modular-first (this project): explicit module boundaries, shared process, local transactions, and durable messaging. Cross-module commands, events, and asynchronous workflows go through messages. Same-store read composition is allowed only as a narrow, visible exception. A same-process write that must share the caller's local transaction may use an explicit owning-module contract. Extract when earned, not by default.
Modules/contains business boundaries such as Catalog, Bookings, and Payments.Delivery/contains concrete downstream capabilities such as booking confirmation email delivery.WebApiis the composition root. It wires boundaries together but does not own business logic.- Cross-module coordination uses Wolverine messages.
- Persistence access is isolated through
DbContextSliceinterfaces instead of a globalAppDbContextdependency from handlers. - Architecture tests enforce the structural rules.
src/
ModularVerticalSlice.Application/
Modules/
Bookings/
Features/
Persistence/
Messages/
Catalog/
Contracts/
Payments/
Delivery/
BookingConfirmation/
Shared/
ModularVerticalSlice.Persistence/
ModularVerticalSlice.SharedKernel/
ModularVerticalSlice.WebApi/
tests/
ModularVerticalSlice.UnitTests/
ModularVerticalSlice.IntegrationTests/
ModularVerticalSlice.ArchitectureTests/
- .NET 10 SDK
- Docker
The repository provides a disposable PostgreSQL instance through docker-compose.yml:
docker compose up -d postgresDefault local credentials:
- Host:
localhost - Port:
5432 - Database:
modularverticalslice - Username:
postgres - Password:
postgres
The design-time DbContext factory requires an explicit connection string when running EF Core commands:
dotnet tool restore
$env:ConnectionStrings__Database = "Host=localhost;Port=5432;Database=modularverticalslice;Username=postgres;Password=postgres"
dotnet ef database update --project .\src\ModularVerticalSlice.Persistencedotnet run --project .\src\ModularVerticalSlice.WebApiIn Development the API listens on:
http://localhost:5211https://localhost:7037
The Development profile also enables a fake authenticated user from appsettings.Development.json. That means booking endpoints can be exercised locally without wiring a real identity provider.
This repository exposes OpenAPI JSON in Development, but it does not include a Swagger UI page.
- OpenAPI document:
http://localhost:5211/openapi/v1.json
If you want to prove the host is running and then exercise one realistic flow, use this sequence.
curl.exe http://localhost:5211/health/live
curl.exe http://localhost:5211/health/ready/health/live is process-only. /health/ready depends on PostgreSQL connectivity.
curl.exe -X POST "http://localhost:5211/api/v1/events/" `
-H "Content-Type: application/json" `
-d "{\"title\":\"Architecture Demo\",\"date\":\"2030-06-01T20:00:00Z\",\"ticketPrice\":25.0,\"availableTickets\":100}"This returns an EventReadModel payload that includes the generated event id.
In Development, fake authentication is already enabled and provides the bookings.write scope required by the booking endpoint.
curl.exe -X POST "http://localhost:5211/api/v1/bookings/" `
-H "Content-Type: application/json" `
-d "{\"eventId\":\"<event-id>\",\"quantity\":2,\"clientRequestId\":\"<new-guid>\"}"This returns the new booking id.
The booking query endpoints use the same Development fake user and scopes:
curl.exe http://localhost:5211/api/v1/events/
curl.exe http://localhost:5211/api/v1/bookings/
curl.exe http://localhost:5211/api/v1/bookings/<booking-id>The public PowerShell script is the main verification contract:
./scripts/verify.ps1It restores dependencies, builds the solution, and runs unit, architecture, and integration tests.
If PostgreSQL is not already running, the script can start the repository's disposable database container and wait for it:
./scripts/verify.ps1 -StartDatabaseNotes:
- the script does not stop or remove containers
- integration tests apply their required migrations
-SkipIntegrationTestsgives faster but incomplete feedback
Hosted CI is optional automation over the same public contract. Any CI workflow should invoke this script so failures stay locally reproducible.
The repository is meant to demonstrate reliability patterns as concrete runtime capabilities, not just as architectural slogans.
| Need | Capability | Why it matters |
|---|---|---|
| durable publish on commit | transactional outbox | prevents lost messages when application state commits |
| safe message receipt | inbox / durable handling | reduces duplicate or partial downstream processing |
| transient fault recovery | retry policies | absorbs temporary infrastructure failures |
| downstream failure containment | circuit breaker | avoids hammering a failing dependency |
| workflow coordination | sagas | makes long-running, multi-step flows explicit |
| delayed follow-up work | scheduled messages | supports timeout and reminder behavior |
| failure inspection | dead-letter queue | preserves failed work for diagnosis and recovery |
Structural boundaries are enforced by ModularVerticalSlice.ArchitectureTests using NetArchTest.
Rules include:
- modules do not reference each other's
FeaturesorDomainnamespaces - delivery boundaries do not act as hidden business modules
- handlers and sagas live in
Applicationnamespaces and do not depend onWebApi WebApidoes not reference module persistence entity typesApplicationdoes not depend on thePersistenceassembly; handlers use only their declaredDbContextSlice
These rules are meant to fail builds when violated. They are not advisory.
Wolverine is both mediator and message bus. Outbox, inbox, retry, circuit breaker, sagas, scheduled messages, and dead-letter queues come from one library instead of being assembled from separate mediator and transport layers.
Queries live in handlers or slice-specific extension methods. Each module declares a DbContextSlice interface exposing only the DbSet<T> or IQueryable<T> it needs. Generic repository abstractions such as GetById and GetAll are intentionally excluded.
Business failures such as NotFound, Conflict, and Validation are modeled as return values, not control-flow exceptions. HTTP mapping is explicit.
This project does not require a separate broker to get outbox, inbox, retry, and dead-letter behavior. Wolverine plus PostgreSQL is the default durable transport. RabbitMQ, Kafka, or Azure Service Bus become relevant only when throughput or topology needs justify them.
ModularVerticalSlice.SharedKernel contains only Result<T>, Error, and ErrorType. A shared kernel that grows without discipline becomes the next monolith.
DbContextSlice adapts the bounded-DbContext idea to a modular monolith without paying the infrastructure cost of separate DbContexts for every module.
The important distinction is that many slices share one AppDbContext instance. That means:
- no extra connection
- no extra roundtrip cost
- no distributed transaction overhead
- access isolation enforced at the type-system level
A handler can only see the tables declared in its slice. Cross-module reads must therefore be explicit. In this solution, GetBookingDetails and GetCustomerBookings use IBookingCatalogReadDbContextSlice to compose Booking and Catalog data in one query without falling back to a global DbContext dependency.
For the full rationale, see ADR-0026.
The host exposes a small operational surface that matches the actual runtime.
GET /health/live: process-only livenessGET /health/ready: readiness based on PostgreSQL connectivity through the applicationAppDbContext
Because Wolverine durable messaging shares the same PostgreSQL dependency, the readiness signal is meaningful for the running application.
- an inbound
X-Correlation-Idheader is honored and echoed back - a non-empty correlation id is generated when the header is absent
- the correlation id flows through HTTP and message handling scopes
- meaningful workflow events are emitted as structured logs
Design decisions with rationale are recorded in docs/adr/.
This repository is released under the MIT License.
ModularVerticalSlice is actively maintained by Antonio Angiò. Contributions are welcome through pull requests.