Zum Hauptinhalt springen
Alle Beiträge
Software Engineering5 Min. Lesezeit

Modularer Monolith: Das Architekturmuster, das Unternehmen vor Microservices in Betracht ziehen sollten

Warum ein modularer Monolith die Bounded-Context-Vorteile von Microservices bietet, ohne die Distributed-System-Kosten — und wann man Services extrahieren sollte.

Veröffentlicht

Die Branche hat ein Jahrzehnt damit verbracht, Microservices zu evangelisieren. Das Pendel schwingt zurück — nicht zu den Monolithen von 2010, sondern zu etwas Pragmatischerem: dem modularen Monolithen.

Ein modularer Monolith bietet Bounded Contexts, klare Ownership und unabhängige Weiterentwicklung — ohne die Distributed-System-Kosten von Netzwerkaufrufen, Eventual Consistency und 47 Kubernetes-Manifesten pro Service.

Dieser Beitrag erklärt, was ein modularer Monolith tatsächlich ist, wie man Modulgrenzen in .NET durchsetzt und wann (nicht ob) man Module in Services extrahieren sollte.

Warum Microservices in den meisten Unternehmen scheitern

Seien wir ehrlich, warum Microservices enttäuschen:

Vorzeitige Zerlegung — Teams teilen ihre Anwendung in Services auf, bevor sie ihre Domänengrenzen verstehen. Das Ergebnis ist ein verteilter Monolith: Services, die nicht unabhängig deployed werden können, weil sie eine Datenbank teilen oder sich synchron für jede Operation aufrufen.

Operativer Overhead — Jeder Service braucht CI/CD, Monitoring, Logging, Health Checks, Service Discovery und eine On-Call-Rotation. Für ein Team von 8 Ingenieuren, das 20 Services verwaltet, verbraucht die Infrastruktur-Steuer mehr Zeit als Feature-Entwicklung.

Datenkonsistenz-Alpträume — Verteilte Transaktionen, Saga-Patterns und Eventual Consistency sind wirklich schwer. Die meisten Teams unterschätzen diese Komplexität, bis sie um 2 Uhr morgens einen Produktionsvorfall debuggen, bei dem drei Services sich über den Status einer Bestellung uneinig sind.

Netzwerk als Zuverlässigkeitsgrenze — Jeder Service-Aufruf ist ein potenzieller Fehlerpunkt. Retry-Logik, Circuit Breaker, Timeouts und Fallbacks fügen Code hinzu, den ein Monolith schlicht nicht braucht.

Nichts davon bedeutet, dass Microservices falsch sind. Sie sind richtig — für spezifische Probleme bei spezifischen Skalen. Der Fehler ist, sie als Standard-Startarchitektur zu übernehmen.

Was ein modularer Monolith tatsächlich ist

Ein modularer Monolith ist eine einzelne Deploy-Einheit, deren interne Struktur das spiegelt, was man mit Microservices bauen würde — ohne die Netzwerkgrenze.

Kernprinzipien

  1. Jedes Modul besitzt einen Bounded Context — Users, Orders, Payments, Inventory sind separate Module mit eigenen Domänenmodellen
  2. Keine geteilten Datenbanktabellen — Jedes Modul hat sein eigenes Schema
  3. Explizite öffentliche API pro Modul — Module kommunizieren über definierte Interfaces, nicht durch Zugriff auf interne Details
  4. Durchgesetzte Grenzen — Der Compiler verhindert Verletzungen, nicht nur Konvention

Die Struktur in .NET

Code
src/
├── Modules/
│   ├── Users/
│   │   ├── Users.Domain/
│   │   ├── Users.Application/
│   │   ├── Users.Infrastructure/
│   │   └── Users.Contracts/
│   ├── Orders/
│   │   ├── Orders.Domain/
│   │   ├── Orders.Application/
│   │   ├── Orders.Infrastructure/
│   │   └── Orders.Contracts/
│   └── Payments/
│       └── Payments.Contracts/
├── Host/
└── Shared/

Modulgrenzen in .NET durchsetzen

Konventionsbasierte Grenzen brechen innerhalb von Wochen. Entwickler unter Zeitdruck nehmen Abkürzungen — es sei denn, der Compiler verhindert es.

Strategie 1: Assembly-Level Access Control

Jedes Modul verwendet standardmäßig internal. Nur das Contracts-Projekt exponiert öffentliche Typen:

Csharp
// Orders.Domain - standardmäßig internal
internal sealed class Order
{
    internal OrderId Id { get; private set; }
    internal CustomerId CustomerId { get; private set; }
    internal OrderStatus Status { get; private set; }
    internal Money TotalAmount { get; private set; }

    internal static Order Create(CustomerId customerId, IReadOnlyList<OrderLine> lines)
    {
        var order = new Order
        {
            Id = OrderId.New(),
            CustomerId = customerId,
            Status = OrderStatus.Created,
            TotalAmount = lines.Sum(l => l.Amount)
        };
        order.AddDomainEvent(new OrderCreatedEvent(order.Id, customerId));
        return order;
    }
}
Csharp
// Orders.Contracts - die einzige öffentliche Oberfläche
public interface IOrderService
{
    Task<OrderDto> GetOrderAsync(Guid orderId);
    Task<Guid> PlaceOrderAsync(PlaceOrderRequest request);
}

public sealed record OrderDto(Guid Id, Guid CustomerId, string Status, decimal Total);

Der Schlüssel: Andere Module können nur Orders.Contracts referenzieren. Sie können Orders.Domain, Orders.Application oder Orders.Infrastructure nicht sehen.

Strategie 2: ArchUnit-Tests

Csharp
[Fact]
public void Orders_Should_Not_Depend_On_Users_Infrastructure()
{
    var result = Types.InAssembly(typeof(Order).Assembly)
        .Should()
        .NotHaveDependencyOn("Users.Infrastructure")
        .GetResult();

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

Strategie 3: Separate DbContexts pro Modul

Csharp
internal sealed class OrdersDbContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderLine> OrderLines => Set<OrderLine>();

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

Modulgrenz-Architektur

Loading diagram...

Extraktions-Entscheidungsfluss

Loading diagram...

Inter-Modul-Kommunikation

Synchron: Interface-basierte Aufrufe

Csharp
public sealed class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand, Guid>
{
    private readonly IUserService _userService;

    public async Task<Guid> Handle(PlaceOrderCommand command, CancellationToken ct)
    {
        var user = await _userService.GetUserAsync(command.CustomerId);
        if (user is null) throw new UserNotFoundException(command.CustomerId);
        // ... Order erstellen
    }
}

Asynchron: Domain Events

Csharp
public sealed record OrderPlacedEvent(Guid OrderId, Guid CustomerId, decimal Amount) : IDomainEvent;

internal sealed class OrderPlacedHandler : IDomainEventHandler<OrderPlacedEvent>
{
    public async Task Handle(OrderPlacedEvent notification, CancellationToken ct)
    {
        // Payment Intent erstellen, Mittel reservieren, etc.
    }
}

Im Monolith werden diese Events in-process via MediatR dispatched. Bei späterer Extraktion tauscht man den In-Process-Dispatcher gegen einen Message Broker — der Handler-Code ändert sich kaum.

Wann ein Modul in einen Microservice extrahieren

Extrahieren wenn:

  • Ein Modul unabhängige Skalierung braucht (z.B. Bildverarbeitung mit GPU)
  • Ein Modul radikal andere Deploy-Kadenz hat (z.B. ML-Modell-Updates täglich)
  • Ein Modul eine andere Technologie erfordert (z.B. Python für ML)
  • Ein Team groß genug gewachsen ist (8+ Ingenieure auf einem Modul)

Nicht extrahieren wenn:

  • „Es fühlt sich an wie ein Service" — Gefühl ist kein Grund
  • „Wir wollen Team-Autonomie" — Modulgrenzen bieten das bereits
  • „Microservices sind Best Practice" — sie sind ein Trade-off
  • Das Modul hohe Kopplung zu anderen Modulen hat

Der Migrationspfad

  1. Das Contracts-Assembly wird ein NuGet-Paket oder gRPC-Proto-Definition
  2. Der In-Process-Event-Dispatcher wird ein Service-Bus-Publisher
  3. Der DbContext des Moduls hat bereits sein eigenes Schema — in separate Datenbank aufteilen
  4. Die DI-Registrierung des Moduls wird die Program.cs eines separaten Service

Das dauert Tage, nicht Monate — weil die harte Domänenzerlegung bereits erledigt ist.

Praxisempfehlung

Wir empfehlen den modularen Monolith als Standard-Architektur für Enterprise-Anwendungen mit:

  • Weniger als 50 Ingenieuren am System
  • Keinen extremen Skalierungsunterschieden zwischen Modulen
  • Einer Domäne, die noch erkundet wird
  • Einem Zeitrahmen, der keine Distributed-System-Infrastruktur-Investition erlaubt

Starten Sie modular. Extrahieren Sie chirurgisch. Lassen Sie Produktions-Traffic-Muster — nicht Architekturdiagramme — Ihnen sagen, wann Distribution notwendig ist.


Möchten Sie eine modulare Architektur für Ihre Enterprise-Anwendung entwerfen? Kontaktieren Sie uns — wir helfen Teams, die richtigen architektonischen Grenzen zu finden, ohne Over-Engineering.

Themen

Modularer MonolithMicroservices-AlternativeBounded Contexts.NET-ArchitekturEnterprise-Anwendungsarchitektur

Häufig gestellte Fragen

Ein modularer Monolith ist eine einzelne deploybare Anwendung, deren interne Struktur in klar definierte Module mit strikter Grenzendurchsetzung unterteilt ist. Jedes Modul besitzt seine eigene Domäne, Daten und API-Oberfläche — ähnlich einem Microservice — aber alle Module laufen im selben Prozess und werden zusammen deployed.

Expert engagement

Brauchen Sie Expertenberatung?

Unser Team ist spezialisiert auf Cloud-Architektur, Security, KI-Plattformen und DevSecOps. Lassen Sie uns besprechen, wie wir Ihrem Unternehmen helfen können.

Kontakt aufnehmenNo commitment · No sales pressure

Verwandte Artikel

Alle Beiträge