DEV Community

Cover image for Monolith, Modular Monolith and Microservices: A Practical Comparison for .NET
Giorgi Mgebrishvili
Giorgi Mgebrishvili

Posted on Edited on Originally published at Medium

Monolith, Modular Monolith and Microservices: A Practical Comparison for .NET

Cover photo by Nicolás Gutiérrez on Unsplash

Most software engineers do not choose their architecture in a vacuum. You usually meet architectures in stages: traditional monoliths in legacy codebases or early-stage products, microservices when a company wants independent team scaling, and modular monoliths when an organization realizes that distributed systems introduce operational costs they did not anticipate.

Each approach is often marketed as the obvious successor to the one before it. In real engineering, however, these architectures are simply points on a trade-off spectrum balancing operational simplicity against structural isolation.

This article compares traditional monoliths, modular monoliths, and microservices across core architectural dimensions, outlines the specific .NET libraries and frameworks each one relies on, and provides practical criteria for choosing between them.


1. What Each Architecture Actually Is

TRADITIONAL MONOLITH
┌───────────────────────────────────────────────┐
│              Single Process                   │
│   Controllers ─── Services ─── Data Access    │
└───────────────────────┬───────────────────────┘
                        ▼
                [Single Database]

MODULAR MONOLITH
┌───────────────────────────────────────────────┐
│              Single Process                   │
│  ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
│  │   Catalog   │ │   Orders    │ │ Invoicing│ │
│  │   Module    │ │   Module    │ │  Module  │ │
│  └──────┬──────┘ └──────┬──────┘ └────┬─────┘ │
│         │ (Isolated Schemas / Tables) │       │
└─────────┼───────────────┼─────────────┼───────┘
          ▼               ▼             ▼
       [catalog.*]    [orders.*]   [invoicing.*]
                (One Database Instance)

MICROSERVICES
┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│ Catalog API  │   │  Orders API  │   │Invoicing API │
│  (Process 1) │   │ (Process 2)  │   │ (Process 3)  │
└──────┬───────┘   └──────┬───────┘   └──────┬───────┘
       │ Network          │ Network          │ Network
       ▼                  ▼                  ▼
 [Catalog DB]        [Orders DB]       [Invoicing DB]
Enter fullscreen mode Exit fullscreen mode

The Traditional Monolith

A traditional monolith compiles into a single executable or deployable unit (such as a single ASP.NET Core Web API project) where all business domains live in the same project or solution. Code is typically organized horizontally by technical concern (Presentation, Business Logic, Data Access). All layers read and write to a single relational database schema without technical barriers preventing cross-domain joins.

The Modular Monolith

A modular monolith is still deployed as a single process to a single host and typically runs against a single database engine. However, the internal codebase is strictly partitioned into business modules (such as Orders, Inventory, Billing).

Modules cannot reference internal classes from other modules. A module exposes only explicit public contracts (interfaces or DTOs), and each module owns its data—often represented by dedicated database schemas (orders.Orders, billing.Invoices) or isolated tables. Direct SQL joins across module tables are prohibited at the code level.

Microservices

Microservices split each business capability into an independently deployable service running in its own operating system process or container. Each microservice strictly owns its private database. No other service can touch that database directly. All communication happens across a physical network using synchronous protocols (HTTP/REST, gRPC) or asynchronous messaging.


2. Comparison Across Key Engineering Axes

Deployment & Operational Overhead

  • Traditional Monolith: Lowest operational complexity. A single CI/CD pipeline, one artifact to build, and trivial local development (dotnet run or pressing F5 in Visual Studio).
  • Modular Monolith: Retains the operational simplicity of the monolith. You build one Docker container or deploy one service, but you enforce internal discipline at compile time.
  • Microservices: High operational complexity. Requires container orchestration (Kubernetes or container apps), API gateways, service discovery, distributed logging, distributed tracing, and independent deployment pipelines for every service.

Code Boundaries & Encapsulation

  • Traditional Monolith: Boundaries erode easily. When a deadline looms, developers frequently bypass boundaries—querying another domain’s tables or injecting another layer's internal service directly, eventually leading to tightly coupled code.
  • Modular Monolith: Boundaries are enforced by compile-time rules (e.g., C# internal modifiers, private project references, or tools like NetArchTest). One module cannot see the internal implementation details of another.
  • Microservices: Boundaries are enforced by physical process boundaries and network firewalls. You cannot take an in-memory shortcut even if you want to.

Data Management & Consistency

  • Traditional & Modular Monolith: Standard database transactions (ACID) work out of the box. If an order creation requires reserving stock, both operations can commit within a single database transaction. Rollbacks are instantaneous and atomic.
  • Microservices: ACID transactions across service boundaries are impossible because databases are isolated. Systems must rely on eventual consistency, asynchronous integration events, the Transactional Outbox pattern, and distributed coordination like the Saga pattern.

Performance & Latency

  • Traditional & Modular Monolith: In-process communication. Calling another domain is an in-memory method invocation executed in nanoseconds without serialization overhead or network packet drops.
  • Microservices: Inter-service calls cross network boundaries. Even on local cloud networks, HTTP/gRPC roundtrips take milliseconds and introduce points of failure that require timeouts, retries, and circuit breakers.

3. Required Libraries and Frameworks by Architecture

Each architectural pattern requires specific libraries in the .NET ecosystem to manage its operational and structural responsibilities.

Architecture Core .NET Libraries & Ecosystem Tools
Traditional Monolith • Entity Framework Core / Dapper
• ASP.NET Core Built-in DI
• FluentValidation
Modular Monolith • NetArchTest.Rules (Architecture tests)
• MediatR / Wolverine (In-process mediator & events)
• EF Core (Multiple DbContext instances / Schema separation)
• Quartz.NET / Hangfire (Internal outbox processing)
Microservices • MassTransit (Message bus abstraction)
• RabbitMQ / Apache Kafka / Redis (Message brokers)
• YARP (Reverse proxy / API Gateway)
• Polly / Microsoft.Extensions.Resilience
• OpenTelemetry (Distributed tracing & metrics)
• gRPC.Net.Client (Synchronous inter-service RPC)

Libraries for the Traditional Monolith

Because a standard monolith does not deal with distributed state or isolated modules, its library footprint is lean:

  • Data Access: Entity Framework Core or Dapper accessing a shared database context.
  • Dependency Injection: The default Microsoft.Extensions.DependencyInjection container.
  • Validation: FluentValidation executed directly in the ASP.NET Core pipeline before reaching controller actions or service methods.

Libraries for the Modular Monolith

A modular monolith requires tooling to enforce encapsulation within a single solution:

  • Architecture Boundary Testing: NetArchTest.Rules. Architecture tests ensure that code in Modules.Orders never references Modules.Billing.Infrastructure. If a developer attempts an illegal cross-reference, the build fails in CI.
  • In-Process Communication: MediatR or Wolverine. Used to dispatch in-memory commands and domain notifications between modules without tightly coupling module implementations.
  • Data Isolation: EF Core configured with separate DbContext classes per module, each mapping to its own database schema (e.g., modelBuilder.HasDefaultSchema("orders")).
  • Background Processing: Hangfire or Quartz.NET to process asynchronous internal module tasks or dispatch internal integration events without requiring an external broker.

Libraries for Microservices

Microservices require an extensive suite of infrastructure libraries to handle network distribution and event streaming:

  • Asynchronous Messaging & Broker Abstractions: MassTransit acts as the primary service bus abstraction over transport brokers like RabbitMQ, Apache Kafka, or Redis. It provides built-in support for the Publisher/Subscriber (Pub/Sub) pattern, consumer retries, and dead-letter queues.
  • The Transactional Outbox: MassTransit’s EF Core Outbox integration or custom SQL workers. When a service saves an entity to its private database, it writes an outbox message in the same local transaction, ensuring guaranteed message dispatch to the broker even if the application crashes.
  • API Gateways: YARP (Yet Another Reverse Proxy). Used to build high-performance API gateways that route client traffic to internal microservice clusters, handle SSL termination, and manage rate limiting.
  • Resilience & Fault Handling: Polly (or Microsoft.Extensions.Resilience in modern .NET). Essential for wrapping synchronous HTTP and gRPC calls with retry policies, exponential backoff, and circuit breakers.
  • Distributed Observability: OpenTelemetry. Essential for injecting correlation IDs (Trace IDs) into HTTP headers and message envelopes, allowing developers to trace a single user request as it traverses five independent microservices.

4. The Microservices Base Library Problem

A common architectural pattern in .NET microservices is creating a shared base library (often distributed as an internal NuGet package, such as Company.Common or Company.Core).

This library typically standardizes:

  1. Message broker registration (MassTransit configurations and RabbitMQ topologies).
  2. Standard error-handling middleware and logging formats.
  3. OpenTelemetry and distributed tracing setup.
  4. Base DTO records and security claim extraction.
                  ┌───────────────────────────────┐
                  │    Shared NuGet Package       │
                  │       "Company.Core"          │
                  │  (MassTransit, Polly, DTOs)   │
                  └───────┬───────────────┬───────┘
                          │               │
            ┌─────────────┴─────┐   ┌─────┴─────────────┐
            ▼                   ▼   ▼                   ▼
    ┌───────────────┐   ┌───────────────┐   ┌───────────────┐
    │  Catalog API  │   │  Orders API   │   │ Invoicing API │
    │   (.NET 8)    │   │   (.NET 9)    │   │   (.NET 8)    │
    │ Uses Core 1.2 │   │ Uses Core 2.0 │   │ Uses Core 1.0 │
    └───────────────┘   └───────────────┘   └───────────────┘
Enter fullscreen mode Exit fullscreen mode

While a shared base library speeds up initial project creation, it introduces structural friction over time:

  • The Distributed Monolith Trap: If your base library contains shared domain models or common business validation rules, any change to business logic requires updating the library, publishing a new NuGet package, and recompiling/redeploying every service. The services are physically separated, but architecturally coupled.
  • Framework Version Lock-in: If Company.Core upgrades to a new major .NET release or switches a core dependency (e.g., upgrading an underlying messaging package), services cannot easily stay on older versions. You end up managing multi-targeting (net8.0; net9.0) across dozens of internal repositories.
  • Contract Evolution: When using the Pub/Sub pattern with RabbitMQ or Kafka, message contracts must remain backwards-compatible. If a shared library changes a property name or data type on a published event, old consumers that have not yet upgraded will fail during deserialization.

Rule of thumb: Keep shared libraries limited to pure infrastructure plumbing (logging, telemetry wire-up, transport primitives). Never put domain logic, cross-module validation, or concrete database entities into a shared package.


5. Architectural Structure (.NET Solution Trees)

1. Traditional Monolith

CompanyApp.sln
├── CompanyApp.WebAPI/            # Controllers, middleware, DI setup
├── CompanyApp.Core/              # Mixed domain entities (Orders, Products, Users)
└── CompanyApp.Infrastructure/    # Single DbContext with all DbSets, repositories
Enter fullscreen mode Exit fullscreen mode

2. Modular Monolith

CompanyApp.sln
├── CompanyApp.Host/              # ASP.NET Core Web API host, wires modules together
├── Modules/
│   ├── Orders/
│   │   ├── Orders.Contracts/     # Public interfaces and DTOs only
│   │   └── Orders.Core/          # Internal entities, OrdersDbContext, handlers
│   ├── Catalog/
│   │   ├── Catalog.Contracts/    # Public interfaces and DTOs only
│   │   └── Catalog.Core/         # Internal entities, CatalogDbContext, handlers
│   └── Invoicing/
│       ├── Invoicing.Contracts/  # Public interfaces and DTOs only
│       └── Invoicing.Core/       # Internal entities, InvoicingDbContext
└── BuildingBlocks/               # NetArchTest, shared message dispatch interfaces
Enter fullscreen mode Exit fullscreen mode

3. Microservices Fleet

# Separate repositories or independent sln files
CatalogService.sln
├── CatalogService.Api/
├── CatalogService.Infrastructure/   # Private Catalog DB
└── Dockerfile

OrdersService.sln
├── OrdersService.Api/
├── OrdersService.Infrastructure/    # Private Orders DB
└── Dockerfile

ApiGateway.sln
└── YarpGateway/                     # Reverse proxy routing

SharedPackages/
└── Company.ServiceDefaults/        # OpenTelemetry, MassTransit setup (NuGet)
Enter fullscreen mode Exit fullscreen mode

6. How to Choose

Architecture is an evaluation of operational constraints, team organization, and scale requirements.

                       Start Here
                           │
                           ▼
          Do you have multiple separate teams
          needing independent release cycles?
                        /     \
                     Yes       No
                     /           \
                    ▼             ▼
             [Microservices]   Is the domain complex with multiple
                               distinct business contexts?
                                    /     \
                                 Yes       No
                                 /           \
                                ▼             ▼
                       [Modular Monolith]   [Traditional Monolith]
Enter fullscreen mode Exit fullscreen mode

Choose a Traditional Monolith if:

  • You are building a small-to-medium business application, a prototype, or an internal tool with straightforward requirements.
  • You have a small team (1–4 developers) where operational simplicity and speed to market are the top priorities.
  • The domain does not have heavily segregated sub-domains, and maintaining multiple project layers would add unnecessary friction.

Choose a Modular Monolith if:

  • You have a growing business domain with distinct areas (e.g., e-commerce with catalog, payments, fulfillment, and accounting) that must not bleed into each other.
  • You want clean boundaries and independent testability, but want to avoid the operational cost of managing Kubernetes clusters, distributed tracing, and network failures.
  • You want the option to extract specific modules into standalone microservices later without rewriting the core business logic.

Choose Microservices if:

  • Your organization has multiple independent engineering teams that need to deploy features on their own schedules without coordinating deployments or risking cross-team merge conflicts.
  • Specific parts of the application have drastically different resource demands (for example, a real-time tracking service that needs 50 container instances alongside a billing service that needs only two).
  • Your team has the dedicated DevOps, infrastructure, and monitoring maturity required to operate distributed networks in production.

This article was originally published on Medium.

Top comments (0)