Hace tiempo —mucho antes de que existiera la etiqueta “modulith”— ya hablaba en charlas y proyectos de algo que consideraba fundamental:
un monolito puede ser saludable si está dividido en módulos de verdad, no en carpetas sueltas ni DLLs interdependientes.
Lo interesante es que, tiempo después, la industria ha puesto nombre a esta idea (modulith / monolito modular), pero sigo encontrando las mismas confusiones:
-
“Si lo separo en DLLs ya es un modulith, ¿no?” No. Un modulith no es un assembly party, es arquitectura con límites fuertes.
-
“¿Si tengo módulos puedo hablar entre ellos directamente?” No. Igual que en microservicios, deben colaborar por contratos, no por acoplamiento accidental.
-
“¿Puedo exponer Domain Events por Kafka o Service Bus?” Sí, pero en cuanto lo haces pasan a ser Integration Events, no Domain Events.
-
“¿Compilará más rápido en Docker si cambio solo un módulo?” Sí, si usas BuildKit + caché de compilación. No, si usas el Dockerfile estándar.
Por eso hoy dejo aquí un ejemplo completo y muy pequeño, en .NET 8, que demuestra lo que ES y lo que NO ES un modulith… y cómo construirlo rápido con Docker sin recompilar todo cada vez.
Artículos relacionados: leer.
¿Qué es realmente un modulith?
Un modulith es:
- Monolito con límites nítidos entre módulos (bounded contexts internos).
- Cada módulo contiene su dominio, su lógica, sus casos de uso.
La app principal actúa como composition root. - Los módulos no se acoplan entre sí: se comunican por interfaces o eventos.
- Se despliega como un solo proceso, pero se estructura como microservicios internos.
NO es:
- Partir tu solución en 20 proyectos .csproj vacíos.
- Tener “Domain”, “Services” y “Data” como cajones desastre.
- Acceder a EF Core del módulo A desde el módulo B “porque es más rápido”.
- Dejar que todo el mundo vea las clases internas de todos.
Mini-modulith en .NET 8 (Orders + Catalog)
// Estructura
src/
ModularMonolith.Api ← composición
ModularMonolith.SharedKernel ← contratos internos / eventos base
ModularMonolith.Orders ← módulo 1
ModularMonolith.Catalog ← módulo 2
// En la API registramos los módulos:
builder.Services
.AddOrdersModule()
.AddCatalogModule();
app.MapOrdersEndpoints();
app.MapCatalogEndpoints();
// Ejemplo de Domain Event interno
public sealed record OrderPlaced(Guid OrderId, decimal Total) : IDomainEvent;
// Ejemplo de Aggregate que dispara el evento
public sealed class Order : IHasDomainEvents
{
public Guid Id { get; private set; }
public decimal Total { get; private set; }
private readonly List<IDomainEvent> _domainEvents = new();
public Order(decimal total)
{
Id = Guid.NewGuid();
Total = total;
_domainEvents.Add(new OrderPlaced(Id, Total));
}
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents;
public void ClearDomainEvents() => _domainEvents.Clear();
}
// Ejemplo de endpoint del módulo Orders
public static class CreateOrderEndpoint
{
public static IResult Handle(HttpContext httpContext)
{
var order = new Order(99.95m);
foreach (var evt in order.DomainEvents)
Console.WriteLine($"[Orders] Domain event => {evt}");
order.ClearDomainEvents();
return Results.Ok(new { order.Id, order.Total });
}
}
// Y el módulo se monta así:
public static class OrdersModule
{
public static IServiceCollection AddOrdersModule(this IServiceCollection services)
=> services;
public static IEndpointRouteBuilder MapOrdersEndpoints(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/orders");
group.MapPost("/", CreateOrderEndpoint.Handle);
return endpoints;
}
}
Docker rápido para moduliths (BuildKit + caching)
El gran desconocido:
sí se puede compilar solo los módulos modificados, incluso dentro de Docker.
# ============================
# ETAPA 1: Build / Publish
# ============================
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copia solo los csproj (cache RESTORE)
COPY src/ModularMonolith.Api/*.csproj src/ModularMonolith.Api/
COPY src/ModularMonolith.SharedKernel/*.csproj src/ModularMonolith.SharedKernel/
COPY src/ModularMonolith.Orders/*.csproj src/ModularMonolith.Orders/
COPY src/ModularMonolith.Catalog/*.csproj src/ModularMonolith.Catalog/
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
dotnet restore src/ModularMonolith.Api/ModularMonolith.Api.csproj
# Copia del código
COPY . .
RUN --mount=type=cache,id=build-cache,target=/src/.build \
dotnet publish src/ModularMonolith.Api/ModularMonolith.Api.csproj \
-c Release -o /app/publish \
/p:IntermediateOutputPath=/src/.build/obj \
/p:BaseIntermediateOutputPath=/src/.build/obj
# ============================
# ETAPA 2: Runtime
# ============================
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "ModularMonolith.Api.dll"]
Build con BuildKit
export DOCKER_BUILDKIT=1
docker build -t modulith-demo .
- Si cambias solo
Orders, Docker recompila únicamente ese módulo. - Si cambias SharedKernel, recompila la cadena afectada.
- Si no cambia ningún
.csproj, el restore va instantáneo.
Esto acelera builds en CI y local, sin migrar a microservicios ni montar pipelines complejos.
¿Por qué sigo hablando de esto?
Porque todavía veo:
-
microservicios en exceso cuando un modulith sería mejor,
-
monolitos enormes que serían moduliths con 4 decisiones bien tomadas,
-
equipos convencidos de que “modular = carpetas”,
-
y gente que aún piensa que Docker obliga a recompilarlo todo cada vez.
Si quieres empezar bien con arquitectura moderna en .NET sin saltar de cabeza al ecosistema distribuido, un modulith es un gran punto de partida.






