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.
Kommentare (2)
I tried this approach and it works perfectly!
I had the same experience, can confirm.
Well written and easy to follow. Keep it up!
Kommentar hinterlassen