Modular-Monolith-Architektur cover image

Modular-Monolith-Architektur

By Julian Krause · Sunday, March 1, 2026 · ~3 min read

Der modulare Monolith gewinnt an Zuspruch, da Teams erkennen, dass Microservices organisatorische Probleme lösen, nicht technische. Wenn Ihr Team klein genug ist, um sich auf eine einzige Codebasis abzustimmen, verschafft Ihnen ein sauber strukturierter Monolith klare Modulgrenzen, Bereitschaft für unabhängige Deployments und keinen der operativen Mehraufwände verteilter Systeme.

Modulstruktur

Jedes Modul ist ein eigenständiger vertikaler Ausschnitt der Anwendung mit eigener Domäne, eigenem Datenzugriff und eigener API-Oberfläche. Module kommunizieren über klar definierte Verträge, niemals über gemeinsame Datenbanktabellen oder direkte Klassenreferenzen.

/src/
├── Postnomic.Host/                  # Composition root
├── Modules/
│   ├── Orders/
│   │   ├── Orders.Api/              # Controllers, DTOs
│   │   ├── Orders.Domain/           # Entities, value objects
│   │   ├── Orders.Infrastructure/   # EF Core, repositories
│   │   └── Orders.Contracts/        # Public interfaces and events
│   ├── Inventory/
│   │   ├── Inventory.Api/
│   │   ├── Inventory.Domain/
│   │   ├── Inventory.Infrastructure/
│   │   └── Inventory.Contracts/
│   └── Shipping/
│       ├── Shipping.Api/
│       ├── Shipping.Domain/
│       ├── Shipping.Infrastructure/
│       └── Shipping.Contracts/

Modulgrenzen durchsetzen

Der wichtigste Aspekt eines modularen Monolithen ist die Durchsetzung von Grenzen. Ohne sie koppeln sich Module nach und nach, bis Sie einen klassischen Monolithen mit Ordnerstruktur haben. Wir erzwingen Grenzen zur Kompilierzeit über Projektreferenzen und zur Testzeit über Architekturtests.

// Architecture test using NetArchTest
[Fact]
public void OrdersModule_ShouldNotReference_InventoryDomain()
{
    var result = Types.InAssembly(typeof(Order).Assembly)
        .ShouldNot()
        .HaveDependencyOn("Inventory.Domain")
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        "Orders module must not directly reference Inventory domain. " +
        "Use Inventory.Contracts instead.");
}

[Fact]
public void Modules_ShouldOnlyCommunicate_ThroughContracts()
{
    var moduleAssemblies = new[]
    {
        typeof(Order).Assembly,
        typeof(InventoryItem).Assembly,
        typeof(Shipment).Assembly
    };

    foreach (var assembly in moduleAssemblies)
    {
        var otherDomains = moduleAssemblies
            .Where(a => a != assembly)
            .Select(a => a.GetName().Name!)
            .ToArray();

        var result = Types.InAssembly(assembly)
            .ShouldNot()
            .HaveDependencyOnAny(otherDomains)
            .GetResult();

        result.IsSuccessful.Should().BeTrue();
    }
}

Kommunikation zwischen Modulen

Module kommunizieren über prozessinterne Events und Query-Schnittstellen, die in den Contracts-Projekten definiert sind. Das hält Module entkoppelt und vermeidet gleichzeitig Netzwerk-Overhead.

// Orders.Contracts — the public API of the Orders module
public interface IOrderQueryService
{
    Task<OrderSummary?> GetOrderSummaryAsync(Guid orderId, CancellationToken ct);
}

public record OrderConfirmedEvent(Guid OrderId, Guid CustomerId, decimal Total);

// Inventory module consumes the contract
public class WhenOrderConfirmed_ReserveStock(InventoryDbContext db)
    : IEventHandler<OrderConfirmedEvent>
{
    public async Task Handle(OrderConfirmedEvent @event, CancellationToken ct)
    {
        // React to order confirmation without depending on Orders.Domain
        await ReserveStockForOrderAsync(@event.OrderId, ct);
    }
}

Separate DbContexts pro Modul

Jedes Modul besitzt sein eigenes Datenbankschema über einen dedizierten DbContext. Obwohl sie sich dieselbe physische Datenbank teilen, bildet jeder Context nur die Tabellen ab, die zu seinem Modul gehören. Das verhindert versehentliche modulübergreifende Abfragen und macht eine spätere Auslagerung in eine eigene Datenbank unkompliziert.

public class OrdersDbContext(DbContextOptions<OrdersDbContext> options) : DbContext(options)
{
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderLine> OrderLines => Set<OrderLine>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema("orders"); // Schema isolation
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(OrdersDbContext).Assembly);
    }
}

Der modulare Monolith ist keine Zwischenstufe auf dem Weg zu Microservices — er ist eine eigenständige, valide Architektur. Viele Teams werden ihre Module nie in separate Dienste auslagern müssen. Wenn Sie es aber doch tun, machen die sauberen Grenzen diese Auslagerung zu einem mechanischen Vorgang statt zu einem schmerzhaften Entflechten.


Comments (2)

Elena Wednesday, March 11, 2026 5:08 PM

I tried this approach and it works perfectly!

Greta Saturday, March 7, 2026 8:08 PM

I had the same experience, can confirm.

Felix Thursday, March 12, 2026 5:08 PM

Well written and easy to follow. Keep it up!

Leave a Comment