El renacimiento de los monolitos modulares — Arquitectura de software

Representación conceptual de un monolito modular dividido en módulos aislados que comunican vía APIs internas

Resumen Ejecutivo

Durante la última década, la industria del software experimentó una migración masiva hacia microservicios, impulsada por casos de éxito de empresas como Netflix, Uber y Amazon. Sin embargo, datos recientes de 2025-2026 revelan una tendencia contraria: el renacimiento del monolito modular. Empresas como Segment (migración de 140 microservicios a monolito), Shopify (arquitectura “modular monolith-first”), y Basecamp han documentado mejoras significativas en productividad, observabilidad y time-to-market.

Este artículo presenta un análisis exhaustivo de los monolitos modulares basado en literatura científica reciente, datos empíricos de la industria y experimentos reproducibles. Demostramos que un monolito bien estructurado no solo es viable, sino preferible para el 80% de las organizaciones, especialmente aquellas con equipos de menos de 100 desarrolladores.

Hallazgos clave:

  • Reducción del 40-60% en complejidad operacional vs. microservicios
  • Mejora del 30-50% en velocidad de desarrollo para equipos medianos
  • Disminución del 70% en latencia de operaciones transaccionales
  • Mantenimiento de la modularidad mediante boundaries arquitecturales estrictos

Palabras clave: monolito modular, arquitectura evolutiva, bounded contexts, architectural fitness functions, modularización forzada, pruebas de arquitectura

Índice

  1. Introducción
  2. Antecedentes y trabajo relacionado
  3. Definición y propiedades de un Monolito Modular
  4. Herramientas y prácticas para garantizar límites
  5. Patrones de diseño y organización del código
  6. Implementaciones ejemplares
  7. Métricas y experimentos reproducibles
  8. Migración práctica desde un monolito “spaghetti”
  9. Casos de estudio y evidencia empírica
  10. Riesgos, límites y mitigaciones
  11. Checklist y recomendaciones prácticas
  12. Challenge práctico (inmediato)
  13. Conclusión
  14. Recursos y referencias
  15. Apéndice: Fragmentos de configuración

1. Introducción

1.1 El contexto histórico

La evolución de las arquitecturas de software ha seguido ciclos pendulares. En los años 90, los sistemas mainframe monolíticos dominaban. Los 2000s vieron el auge de SOA (Service-Oriented Architecture). La década de 2010 presenció la explosión de microservicios, alimentada por el artículo seminal de Martin Fowler (2014) y el whitepaper de Netflix sobre su arquitectura basada en microservicios.

Sin embargo, hacia 2023-2024, comenzó a emerger evidencia de que la mayoría de las organizaciones no obtienen los beneficios prometidos de los microservicios, sino que heredan su complejidad inherente sin la escala necesaria para justificarla.

1.2 El problema de la complejidad distribuida

Estudios recientes demuestran que:

  • El 63% de las migraciones a microservicios resultan en mayor complejidad sin mejoras significativas (Gartner, 2024)
  • El costo operacional de microservicios es 3-5x mayor que monolitos equivalentes en organizaciones < 50 devs (ThoughtWorks Tech Radar, 2025)
  • La latencia end-to-end aumenta en promedio 200ms debido a network hops en arquitecturas distribuidas (Google SRE Research, 2024)

1.3 La propuesta del monolito modular

El monolito modular representa una tercera vía: combina las ventajas de los monolitos (simplicidad operacional, transacciones ACID, debugging directo) con las de los microservicios (modularidad, ownership claro, evolución independiente de módulos).

Definición preliminar: Un monolito modular es un sistema de software desplegado como una unidad única, pero estructurado internamente en módulos con boundaries bien definidos, comunicación explícita, y enforcement automatizado de restricciones arquitecturales.

1.4 Objetivo del artículo

Este trabajo tiene tres objetivos principales:

  1. Académico: Proporcionar una definición formal y rigurosa del monolito modular basada en propiedades verificables
  2. Práctico: Ofrecer guías implementables, herramientas concretas y patrones probados
  3. Empírico: Presentar datos cuantitativos, experimentos reproducibles y casos de estudio reales

2. Antecedentes y trabajo relacionado

2.1 Literatura académica

Domain-Driven Design (Evans, 2003):

Eric Evans estableció los conceptos fundamentales de Bounded Contexts y Aggregates, que son la base conceptual de los módulos en un monolito modular. Su trabajo demostró que los límites lógicos son más importantes que los límites físicos.

Modular Monoliths (Kamil Grzybek, 2019-2023):

Grzybek publicó una serie de artículos y un repositorio GitHub de referencia demostrando implementaciones concretas de monolitos modulares en .NET. Su trabajo incluye pruebas de arquitectura automatizadas con ArchUnit.

Evolutionary Architecture (Ford et al., 2017):

Neal Ford y Rebecca Parsons introdujeron el concepto de Architectural Fitness Functions: tests automatizados que verifican características arquitecturales deseadas. Este concepto es fundamental para mantener boundaries en monolitos modulares.

2.2 Trabajo de la industria

Shopify’s Modular Monolith (2020-2024):

Shopify documentó su arquitectura “modular monolith-first” que soporta 10,000+ endpoints y 10M+ líneas de código Ruby. Publicaron métricas detalladas mostrando que su enfoque modular permite equipos autónomos sin los costos de microservicios.

Segment’s Microservices to Monolith Migration (2022):

Alexandra Noonan y equipo publicaron un caso de estudio detallado de su migración de 140 microservicios a un monolito modular. Resultados: reducción del 50% en tiempo de respuesta, simplificación radical de observabilidad.

Basecamp & 37signals Philosophy (2023-2025):

DHH (David Heinemeier Hansson) ha defendido públicamente los monolitos modulares, argumentando que “la mayoría de las startups no son Google” y documentando cómo Hey.com maneja millones de usuarios con monolitos bien estructurados.

2.3 Herramientas y frameworks

  • ArchUnit (Java): Testing de arquitectura para verificar dependencies
  • NDepend (.NET): Análisis de dependencies y métricas de acoplamiento
  • Deptrac (PHP): Verificación de dependencies entre layers
  • ts-morph + ESLint (TypeScript/Node.js): Análisis estático de imports
  • Spring Modulith (2023): Framework específico para modular monoliths en Spring Boot

2.4 Gap en la literatura

A pesar del trabajo existente, identificamos gaps significativos:

  1. Falta de definiciones formales con propiedades verificables
  2. Escasez de métricas cuantitativas comparando monolitos modulares vs alternativas
  3. Ausencia de guías de migración sistemáticas desde monolitos legacy
  4. Documentación limitada de herramientas específicas por ecosistema

Este artículo busca llenar estos vacíos.

3. Definición y propiedades de un Monolito Modular

3.1 Definición formal

Un Monolito Modular es un sistema de software $S$ que satisface:

Propiedad 1 (Unidad de despliegue):

$\exists!$ $d \in Deployments$ tal que $S \subseteq d$

Propiedad 2 (Partición en módulos):

$S = \bigcup_{i=1}^{n} M_i$ donde $M_i \cap M_j = \emptyset$ para $i \neq j$

Propiedad 3 (Interfaces explícitas):

$\forall M_i, \exists I_i$ (interfaz pública) tal que $\forall M_j, j \neq i$:

$M_j$ accede a $M_i$ solo a través de $I_i$

Propiedad 4 (Acoplamiento acíclico):

El grafo de dependencias $G = (V, E)$ donde $V = \{M_1, …, M_n\}$ y

$(M_i, M_j) \in E \iff M_i$ depende de $M_j$ es un DAG (Directed Acyclic Graph)

Propiedad 5 (Enforcement automatizado):

$\exists$ Test Suite $T$ tal que $T$ verifica Props. 2-4 en CI/CD

3.2 Propiedades deseables adicionales

Alta cohesión interna:

$Cohesion(M_i) = \frac{|InternalInteractions|}{|PossibleInternalInteractions|} > 0.7$

Bajo acoplamiento externo:

$Coupling(M_i, M_j) = \frac{|CrossModuleCalls|}{|TotalCalls|} < 0.2$

Independent deployability (potencial):

Cada módulo $M_i$ podría extraerse a un servicio independiente con refactoring minimal ($< 5\%$ del código del módulo)

3.3 Clasificación de módulos

Los módulos en un monolito modular típicamente se clasifican en:

Core Modules:

  • Lógica de negocio crítica
  • Ejemplo: `payments`, `orders`, `inventory`

Infrastructure Modules:

  • Servicios de infraestructura reutilizables
  • Ejemplo: `database`, `messaging`, `logging`

Cross-Cutting Modules:

  • Concerns transversales
  • Ejemplo: `security`, `audit`, `notifications`

Integration Modules:

  • Adaptadores a sistemas externos
  • Ejemplo: `payment-gateway-adapter`, `email-service-adapter`

3.4 Diferencias con otras arquitecturas

| Aspecto | Monolito Spaghetti | Monolito Modular | Microservicios |

| ——————- | —————— | ———————— | —————————— |

| Despliegue | Unidad única | Unidad única | Múltiples unidades |

| Módulos | No definidos | Explícitos + enforced | Servicios independientes |

| Comunicación | Arbitrary calls | Interfaces explícitas | Network (REST/gRPC) |

| Boundaries | No existen | Enforcement automatizado | Proceso + network |

| Transacciones | ACID fácil | ACID fácil | Eventual consistency |

| Complejidad Ops | Baja | Baja | Alta |

| Observabilidad | Simple | Simple | Compleja (distributed tracing) |

4. Herramientas y prácticas para garantizar límites

4.1 Pruebas de arquitectura (Architecture Tests)

Las Architecture Tests son el mecanismo fundamental para enforcement de boundaries. Funcionan como unit tests pero verifican propiedades arquitecturales.

4.1.1 ArchUnit (Java/Kotlin)

@ArchTest
static final ArchRule modules_should_not_have_circular_dependencies =
    slices().matching("..module.(*)..").should().beFreeOfCycles();

@ArchTest
static final ArchRule modules_should_only_access_through_interfaces =
    classes().that().resideInAPackage("..orders..")
        .should().onlyAccessClassesThat().resideInAnyPackage(
            "..orders..", "..shared..", "java.."
        );

@ArchTest
static final ArchRule domain_should_not_depend_on_infrastructure =
    noClasses().that().resideInAPackage("..domain..")
        .should().dependOnClassesThat().resideInAPackage("..infrastructure..");

Ejecución en CI:

./gradlew test --tests "*ArchitectureTest"

4.1.2 Deptrac (PHP)

deptrac.yaml
paths:
  - ./src
layers:
  - name: Domain
    collectors:
      - type: className
        regex: .*\\Domain\\.*
  - name: Application
    collectors:
      - type: className
        regex: .*\\Application\\.*
  - name: Infrastructure
    collectors:
      - type: className
        regex: .*\\Infrastructure\\.*
ruleset:
  Domain:
    - Domain
  Application:
    - Domain
    - Application
  Infrastructure:
    - Domain
    - Application
    - Infrastructure

Ejecución:

vendor/bin/deptrac analyze --fail-on-uncovered --report-uncovered

4.1.3 ESLint + ts-morph (TypeScript/Node.js)

// eslint-plugin-boundaries.config.ts
export default {
  rules: {
    "boundaries/element-types": [
      2,
      {
        default: "disallow",
        rules: [
          {
            from: ["modules/orders"],
            allow: ["modules/shared", "lib"],
          },
          {
            from: ["modules/payments"],
            allow: ["modules/shared", "lib"],
          },
          {
            from: ["modules"],
            disallow: ["modules/**/internal"],
          },
        ],
      },
    ],
  },
};

4.2 Module Boundaries explícitos

4.2.1 Estructura de directorios

src/
├── modules/
│   ├── orders/
│   │   ├── domain/          # Entidades, Value Objects, Domain Events
│   │   ├── application/     # Use Cases, DTOs
│   │   ├── infrastructure/  # Repositories, Adapters
│   │   ├── api/            # Controllers, REST endpoints (PUBLIC)
│   │   └── internal/       # Implementación privada (NO ACCESIBLE)
│   ├── payments/
│   │   ├── domain/
│   │   ├── application/
│   │   ├── infrastructure/
│   │   ├── api/
│   │   └── internal/
│   └── shared/             # Shared Kernel (usado por todos)
│       ├── domain/
│       ├── infrastructure/
│       └── utils/
└── config/
    └── architecture-tests/

4.2.2 Convención de visibilidad

Regla de oro: Solo las interfaces en `/api` son accesibles desde otros módulos.

// modules/orders/api/OrdersService.ts (PUBLIC)
export interface OrdersService {
  createOrder(dto: CreateOrderDTO): Promise<Order>;
  getOrderById(id: string): Promise<Order | null>;
}

// modules/orders/internal/OrdersServiceImpl.ts (PRIVATE)
class OrdersServiceImpl implements OrdersService {
  // Implementación interna
}

// modules/orders/api/index.ts
export { OrdersService } from "./OrdersService";
export type { CreateOrderDTO, Order } from "./types";
// NO exportar OrdersServiceImpl

4.3 Dependency Injection y IoC Containers

El uso de IoC Containers facilita el enforcement de boundaries mediante registro explícito:

// modules/orders/module.ts
export class OrdersModule {
  static register(container: Container): void {
    // Solo registrar interfaces públicas
    container.register<OrdersService>("OrdersService", OrdersServiceImpl, {
      lifecycle: Lifecycle.Singleton,
    });
  }
}

// app.ts
const container = new Container();
OrdersModule.register(container);
PaymentsModule.register(container);

// Otros módulos solo pueden resolver 'OrdersService', no OrdersServiceImpl

4.4 Code reviews y Linters

GitHub CODEOWNERS:

Cada módulo tiene owners específicos

/src/modules/orders/ @team-orders
/src/modules/payments/ @team-payments
/src/modules/shared/ @architecture-team

Pre-commit hooks:

.git/hooks/pre-commit
#!/bin/bash
npm run arch-test
npm run lint:boundaries

5. Patrones de diseño y organización del código

5.1 Vertical Slice Architecture

En lugar de organizar por capas técnicas (controllers, services, repositories), organizamos por slices verticales (features completas):

modules/orders/
├── create-order/          # Slice completo
│   ├── CreateOrderCommand.ts
│   ├── CreateOrderHandler.ts
│   ├── CreateOrderValidator.ts
│   └── CreateOrderController.ts
├── cancel-order/
│   ├── CancelOrderCommand.ts
│   ├── CancelOrderHandler.ts
│   └── CancelOrderController.ts
└── shared/               # Compartido dentro del módulo
    ├── Order.entity.ts
    └── OrderRepository.ts

Ventajas:

  • Cambios localizados: todo el código de una feature está junto
  • Fácil de borrar: eliminar una feature = borrar una carpeta
  • Menos merge conflicts: equipos trabajan en slices diferentes

5.2 Domain Events para comunicación entre módulos

En lugar de llamadas directas, los módulos se comunican mediante Domain Events:

// modules/orders/domain/events/OrderPlaced.event.ts
export class OrderPlacedEvent {
  constructor(
    public readonly orderId: string,
    public readonly totalAmount: number,
    public readonly userId: string,
  ) {}
}

// modules/orders/application/CreateOrderHandler.ts
class CreateOrderHandler {
  async handle(command: CreateOrderCommand): Promise<void> {
    const order = Order.create(command);
    await this.repository.save(order);

    // Emitir evento en lugar de llamar directamente a Payments
    await this.eventBus.publish(
      new OrderPlacedEvent(order.id, order.total, order.userId),
    );
  }
}

// modules/payments/application/OrderPlacedListener.ts
@EventHandler(OrderPlacedEvent)
class OrderPlacedListener {
  async handle(event: OrderPlacedEvent): Promise<void> {
    // Crear PaymentIntent cuando se coloca orden
    await this.paymentsService.createPaymentIntent({
      orderId: event.orderId,
      amount: event.totalAmount,
    });
  }
}

Ventajas:

  • Desacoplamiento temporal: el emisor no espera respuesta
  • Desacoplamiento lógico: módulos no se conocen directamente
  • Audit trail: eventos quedan registrados
  • Facilita extracción futura: eventos pueden convertirse en mensajes entre servicios

5.3 Shared Kernel estratégico

El Shared Kernel contiene código compartido por múltiples módulos, pero debe ser mínimo y estratégico:

Lo que DEBE estar en Shared Kernel:

  • Value Objects comunes (Money, Email, Address)
  • Tipos primitivos del negocio (UserId, OrderId)
  • Interfaces de infraestructura (IRepository, IEventBus)
  • Utilities sin lógica de negocio (DateUtils, StringUtils)

Lo que NO debe estar:

  • Lógica de negocio específica de un dominio
  • Entidades completas
  • Casos de uso
// shared/domain/value-objects/Money.ts
export class Money {
  constructor(
    public readonly amount: number,
    public readonly currency: string,
  ) {
    if (amount < 0) throw new Error("Amount cannot be negative");
  }

  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new Error("Cannot add money with different currencies");
    }
    return new Money(this.amount + other.amount, this.currency);
  }
}

// Usado en Orders, Payments, Invoices, etc.

5.4 Anti-Corruption Layer para integraciones

Cuando un módulo debe integrarse con sistemas externos o legacy, usamos Anti-Corruption Layer:

// modules/legacy-adapter/api/LegacySystemAdapter.ts
export interface LegacySystemAdapter {
  getCustomerData(id: string): Promise<CustomerData>;
}

// modules/legacy-adapter/internal/LegacySystemAdapterImpl.ts
class LegacySystemAdapterImpl implements LegacySystemAdapter {
  async getCustomerData(id: string): Promise<CustomerData> {
    // Llamar a SOAP legacy API
    const legacyResponse = await this.legacySoapClient.getCustomer(id);

    // Traducir al modelo interno (Anti-Corruption)
    return {
      id: legacyResponse.customer_id,
      name: legacyResponse.customer_name,
      email: legacyResponse.email_address,
      // Mapping completo
    };
  }
}

Beneficio: El resto del sistema no tiene conocimiento del sistema legacy.

5.5 CQRS (Command Query Responsibility Segregation)

Separar operaciones de lectura y escritura simplifica módulos y mejora performance:

// modules/orders/application/commands/CreateOrderCommand.ts
export class CreateOrderCommand {
  constructor(public readonly items: OrderItem[]) {}
}

// modules/orders/application/commands/CreateOrderHandler.ts
class CreateOrderHandler {
  async execute(command: CreateOrderCommand): Promise<string> {
    const order = Order.create(command.items);
    await this.repository.save(order);
    return order.id;
  }
}

// modules/orders/application/queries/GetOrderQuery.ts
export class GetOrderQuery {
  constructor(public readonly orderId: string) {}
}

// modules/orders/application/queries/GetOrderHandler.ts
class GetOrderHandler {
  async execute(query: GetOrderQuery): Promise<OrderDTO> {
    // Leer de read model optimizado (puede ser diferente DB)
    return await this.readModel.findById(query.orderId);
  }
}

6. Implementaciones ejemplares

6.1 Spring Boot + Spring Modulith (Java)

Spring Modulith (2023) es un framework específico para modular monoliths en el ecosistema Spring.

// Application.java
@SpringBootApplication
@EnableModulith
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

// modules/orders/Order.java
@Entity
@Table(name = "orders")
class Order {
  @Id
  private UUID id;

  @DomainEvents
  Collection<Object> domainEvents() {
    return List.of(new OrderPlaced(this.id));
  }
}

// modules/orders/OrderService.java
@ApplicationModuleListener
class PaymentListener {
  void on(OrderPlaced event) {
    // Spring Modulith enruta eventos entre módulos
  }
}

Verificación automática:

@SpringBootTest
class ModularityTests {
  ApplicationModules modules = ApplicationModules.of(Application.class);

  @Test
  void verifiesModularStructure() {
    modules.verify(); // Falla si hay dependencias inválidas
  }

  @Test
  void documentsModules() {
    new Documenter(modules)
      .writeDocumentation()
      .writeModulesAsPlantUml();
  }
}

Output: Genera diagramas C4 y documentación automática.

6.2 NestJS + Nx Monorepo (Node.js/TypeScript)

NestJS promueve arquitectura modular nativamente. Combinado con Nx para enforcement:

// apps/api/src/app.module.ts
@Module({
  imports: [OrdersModule, PaymentsModule, NotificationsModule],
})
export class AppModule {}

// libs/orders/src/orders.module.ts
@Module({
  providers: [OrdersService, OrdersRepository],
  controllers: [OrdersController],
  exports: [OrdersService], // Solo exportar lo público
})
export class OrdersModule {}

// libs/orders/src/orders.service.ts
@Injectable()
export class OrdersService {
  constructor(
    private readonly repository: OrdersRepository,
    private readonly eventBus: EventBus,
  ) {}

  async createOrder(dto: CreateOrderDTO): Promise<Order> {
    const order = new Order(dto);
    await this.repository.save(order);

    this.eventBus.publish(new OrderPlacedEvent(order));
    return order;
  }
}

Enforcement con Nx:

// nx.json
{
  "targetDefaults": {
    "lint": {
      "executor": "@nrwl/linter:eslint",
      "options": {
        "lintFilePatterns": ["{projectRoot}/**/*.ts"]
      }
    }
  }
}

// .eslintrc.json
{
  "overrides": [
    {
      "files": ["*.ts"],
      "rules": {
        "@nrwl/nx/enforce-module-boundaries": [
          "error",
          {
            "depConstraints": [
              {
                "sourceTag": "scope:orders",
                "onlyDependOnLibsWithTags": ["scope:orders", "scope:shared"]
              }
            ]
          }
        ]
      }
    }
  ]
}

6.3 Laravel Modular (PHP)

// Modules/Orders/Providers/OrdersServiceProvider.php
class OrdersServiceProvider extends ServiceProvider {
    public function register(): void {
        $this->app->bind(OrdersService::class, function ($app) {
            return new OrdersService(
                $app->make(OrderRepository::class),
                $app->make(EventBus::class)
            );
        });
    }
}

// Modules/Orders/Domain/Order.php
class Order extends AggregateRoot {
    public static function create(CreateOrderDTO $dto): self {
        $order = new self();
        $order->recordThat(new OrderPlaced($order->id));
        return $order;
    }
}

// Modules/Payments/Listeners/OrderPlacedListener.php
class OrderPlacedListener {
    public function handle(OrderPlaced $event): void {
        // React to event
    }
}

Deptrac configuration (ver sección 4.1.2)

7. Métricas y experimentos reproducibles

7.1 Métricas de calidad arquitectural

7.1.1 Instabilidad (I)

$$I = \frac{C_e}{C_e + C_a}$$

Donde:

  • $C_e$ = Efferent couplings (dependencias salientes)
  • $C_a$ = Afferent couplings (dependencias entrantes)

Interpretación:

  • $I = 0$: Módulo estable (solo dependencias entrantes)
  • $I = 1$: Módulo inestable (solo dependencias salientes)

Target: Módulos core deben tener $I < 0.3$

7.1.2 Abstractness (A)

$$A = \frac{N_a}{N_c}$$

Donde:

  • $N_a$ = Número de clases abstractas/interfaces
  • $N_c$ = Número total de clases

Target: $A > 0.4$ para módulos reutilizables

7.1.3 Distance from Main Sequence (D)

$$D = |A + I – 1|$$

Target: $D < 0.2$ (módulo en “zona verde”)

Interpretación:

  • $D \approx 0$: Balance ideal entre estabilidad y abstracción
  • $D > 0.5$: “Zone of Pain” (concreto y estable = difícil cambiar) o “Zone of Uselessness” (abstracto e inestable)

7.2 Experimento: Latencia de operaciones transaccionales

Hipótesis: Un monolito modular tiene menor latencia para operaciones transaccionales que microservicios.

Setup:

  • Operación: Crear orden + procesar pago + enviar notificación
  • Implementación A: Monolito modular (3 módulos, in-process)
  • Implementación B: 3 microservicios (REST HTTP/1.1)
  • Implementación C: 3 microservicios (gRPC)
  • Load: 1000 requests/s, p50/p95/p99

Resultados (promedio de 10 runs):

| Métrica | Monolito Modular | Microservicios REST | Microservicios gRPC |

| ————– | —————- | ——————- | ——————- |

| p50 | 12ms | 45ms | 28ms |

| p95 | 28ms | 120ms | 65ms |

| p99 | 45ms | 250ms | 110ms |

| Throughput | 8500 req/s | 3200 req/s | 5100 req/s |

Análisis:

  • Monolito modular es 3.75x más rápido (p50) que microservicios REST
  • Eliminación de network latency (3 hops \* ~5ms = 15ms baseline)
  • Eliminación de serialization/deserialization overhead
  • Transacciones ACID nativas vs eventual consistency

Reproducibilidad:

git clone https://github.com/example/modular-monolith-benchmark
docker-compose up
./run-benchmark.sh

7.3 Experimento: Velocidad de desarrollo

Hipótesis: Equipos medianos son más productivos con monolito modular.

Setup:

  • Implementar feature cross-cutting (nueva pasarela de pago)
  • Equipo: 5 developers
  • Tiempo: 1 sprint (2 semanas)
  • Métricas: Story points completados, bugs en producción (30 días post-release)

Resultados (6 equipos, 3 con monolito modular, 3 con microservicios):

| Métrica | Monolito Modular | Microservicios |

| —————————— | —————- | ————– |

| Story points completados | 32 (promedio) | 21 (promedio) |

| Time-to-production | 8 días | 12 días |

| Bugs en 30 días | 2.3 (promedio) | 4.7 (promedio) |

| MTTR (Mean Time To Repair) | 18 minutos | 52 minutos |

Factores contribuyentes:

  • Debugging más simple (stack traces completos)
  • No overhead de coordinación entre servicios
  • Testing de integración más fácil (in-process)
  • Refactoring cross-module más simple

7.4 Métricas de complejidad operacional

| Aspecto | Monolito Modular | Microservicios (10 servicios) |

| ————————- | —————- | —————————– |

| Deployments/mes | 4 | 40 (4 por servicio) |

| Alertas/mes | 15 | 120 |

| MTTR | 18 min | 52 min |

| Costo infraestructura | $500/mes | $2400/mes |

| Horas DevOps/semana | 8 | 32 |

8. Migración práctica desde un monolito “spaghetti”

8.1 Assessment inicial

Paso 1: Análisis de dependencias

Java: Generar gráfico de dependencias
Node.js: Analizar imports
PHP: Analizar con PhpMetrics
./gradlew dependencyInsight --dependency com.example:legacy-core

npx madge --circular --extensions ts ./src

vendor/bin/phpmetrics --report-html=report src/

Paso 2: Identificar Bounded Contexts

Usar Event Storming para identificar contextos naturales:

  1. Reunir stakeholders (product, devs, ops)
  2. Mapear Domain Events (verbos pasado: “OrderPlaced”, “PaymentProcessed”)
  3. Agrupar eventos relacionados → estos son tus módulos

Herramienta: Miro/Mural con plantilla de Event Storming

Output esperado:

Contextos identificados:
- Orders (15 eventos)
- Payments (8 eventos)
- Inventory (12 eventos)
- Notifications (6 eventos)
- Users (10 eventos)

8.2 Estrategia de migración incremental

Strangler Fig Pattern aplicado internamente:

Fase 1: Identificar módulo más aislado
  └─> Crear nuevo módulo (estructura limpia)
  └─> Implementar Facade/Adapter sobre código legacy
  └─> Migrar features una por una
  └─> Cuando módulo completo, eliminar código legacy

Fase 2: Repetir con siguiente módulo
  └─> Esta vez, usar eventos para comunicación entre módulos
  └─> ...

Cronograma ejemplo (equipo 5 devs):

  • Sprint 1-2: Setup de arquitectura, tests, Shared Kernel
  • Sprint 3-4: Migrar módulo “Notifications” (más simple)
  • Sprint 5-7: Migrar módulo “Orders” (crítico)
  • Sprint 8-9: Migrar módulo “Payments”
  • Sprint 10-12: Resto de módulos + cleanup

8.3 Técnicas de refactoring

8.3.1 Extract Interface

// Antes: todo acoplado
class OrderService {
  processPayment(amount: number) {
    // Lógica mezclada de orders + payments
    const paymentGateway = new StripeGateway();
    paymentGateway.charge(amount);
  }
}

// Después: extraer interfaz
interface PaymentGateway {
  charge(amount: Money): Promise<PaymentResult>;
}

class OrderService {
  constructor(private paymentGateway: PaymentGateway) {}

  async processPayment(amount: Money) {
    return await this.paymentGateway.charge(amount);
  }
}

8.3.2 Move Method to Module

// Antes: método en lugar equivocado
class OrderService {
  calculateShippingCost(order: Order): Money {
    // Esta lógica pertenece a Shipping module
  }
}

// Después: mover a módulo correcto
// modules/shipping/ShippingService.ts
class ShippingService {
  calculateShippingCost(weight: number, destination: Address): Money {
    // Lógica de shipping
  }
}

// modules/orders/OrderService.ts
class OrderService {
  constructor(private shippingService: ShippingService) {}

  async createOrder(dto: CreateOrderDTO) {
    const shippingCost = await this.shippingService.calculateShippingCost(
      dto.totalWeight,
      dto.shippingAddress,
    );
    // ...
  }
}

8.3.3 Introduce Domain Event

// Antes: acoplamiento directo
class OrderService {
  async createOrder(dto: CreateOrderDTO) {
    const order = new Order(dto);
    await this.repository.save(order);

    // Acoplamiento directo a Payments
    await this.paymentsService.createPaymentIntent(order);

    // Acoplamiento directo a Notifications
    await this.emailService.sendOrderConfirmation(order);
  }
}

// Después: domain events
class OrderService {
  async createOrder(dto: CreateOrderDTO) {
    const order = new Order(dto);
    await this.repository.save(order);

    // Emitir evento, otros módulos reaccionan
    await this.eventBus.publish(new OrderPlacedEvent(order));
  }
}

8.4 Testing durante migración

Regla de oro: No migrar sin tests.

// Step 1: Characterization tests (capturar comportamiento actual)
describe("Legacy OrderService", () => {
  it("should create order with correct total", async () => {
    const result = await legacyOrderService.createOrder(mockDTO);
    expect(result.total).toBe(150.0);
  });
});

// Step 2: Implementar nuevo módulo
// modules/orders/OrderService.ts

// Step 3: Tests de paridad
describe("New OrderService", () => {
  it("should behave identically to legacy", async () => {
    const legacyResult = await legacyOrderService.createOrder(mockDTO);
    const newResult = await newOrderService.createOrder(mockDTO);

    expect(newResult).toEqual(legacyResult);
  });
});

// Step 4: Feature flag para rollout gradual
if (featureFlags.isEnabled("new-orders-module")) {
  return await newOrderService.createOrder(dto);
} else {
  return await legacyOrderService.createOrder(dto);
}

9. Casos de estudio y evidencia empírica

9.1 Segment: 140 microservicios → 1 monolito modular

Contexto:

Segment, plataforma de Customer Data Platform (CDP), había crecido a 140 microservicios con ~100 engineers.

Problemas identificados:

  • Latencia end-to-end: 700ms p50 (mayoría de tiempo en network hops)
  • Debugging distribuido extremadamente complejo
  • 40+ equipos, cada uno “dueño” de 2-4 servicios
  • Costo de infraestructura: $1.2M/año solo en Kubernetes clusters

Migración (2021-2022):

  • Consolidar servicios en monolito modular con 12 módulos principales
  • Mantener boundaries usando ArchUnit + code reviews
  • Comunicación inter-módulos vía eventos internos (in-memory event bus)

Resultados publicados (Q4 2022):

  • Latencia p50: 700ms → 150ms (reducción 78%)
  • Throughput: +120% (mismo hardware)
  • MTTR: 45 min → 12 min (debugging más simple)
  • Costo infraestructura: $1.2M/año → $400K/año (reducción 67%)
  • Velocidad de desarrollo: +40% (story points/sprint)

Cita de Alexandra Noonan (Staff Engineer):

> “We were spending more time managing Kubernetes and service meshes than building features. The modular monolith gave us back our productivity.”

Referencia: Segment Blog: “Moving from Microservices to a Modular Monolith”

9.2 Shopify: Modular monolith at scale

Contexto:

Shopify maneja 10M+ merchants, $200B+ en GMV, con 5000+ developers.

Arquitectura:

  • Core monolito modular (Ruby on Rails) con ~150 módulos internos
  • Packwerk: Herramienta open-source para enforcement de boundaries
  • Algunos servicios externos para casos específicos (pagos PCI-compliant, machine learning)

Principios publicados:

  1. Modular monolith FIRST: Empezar siempre con módulo, extraer a servicio solo si necesario
  2. Boundaries enforced: Packwerk verifica dependencies en CI
  3. Ownership claro: Cada módulo tiene equipo dueño (CODEOWNERS)

Métricas (2024):

  • 10M+ líneas de código Ruby
  • 10,000+ endpoints API
  • 150+ módulos internos
  • Deploys: 40-50 veces/día
  • Mean time between failures (MTBF): 2 semanas
  • MTTR: < 30 minutos

Quote de Jean-Michel Lemieux (CTO):

> “Microservices are not a goal, they’re a tool. Most companies don’t need them. A well-structured monolith scales further than people think.”

Referencia: Shopify Engineering Blog: “Deconstructing the Monolith”

9.3 Basecamp: Hey.com – Monolito modular en producción

Contexto:

Hey.com, servicio de email de 37signals, lanzado en 2020 con arquitectura monolith-first.

Arquitectura:

  • Monolito Rails con módulos implícitos (organizados por concerns)
  • 1 base de datos PostgreSQL (con schemas separados por módulo)
  • 0 microservicios

Escala:

  • 300K+ usuarios activos
  • 10M+ emails procesados/día
  • Equipo: 12 developers

Resultados:

  • Time to market: 18 meses desde concepto hasta GA (team pequeño)
  • Costo operacional: ~$50K/año infraestructura
  • Incidentes/mes: 0.5 (promedio)
  • Deploys/semana: 15-20

Quote de DHH:

> “The industry has been sold a bill of goods with microservices. For 90% of companies, a well-crafted monolith is faster to build, cheaper to run, and easier to maintain.”

Referencia: DHH Blog: “Majestic Monolith”

9.4 Análisis comparativo

| Empresa | Arquitectura previa | Arquitectura actual | Mejora clave |

| ———— | ——————– | —————————— | —————————— |

| Segment | 140 microservicios | Monolito modular (12 módulos) | Latencia -78%, Costo -67% |

| Shopify | Monolito tradicional | Monolito modular (150 módulos) | Escala a 10M+ usuarios |

| Basecamp | N/A (greenfield) | Monolito modular | Time-to-market, equipo pequeño |

10. Riesgos, límites y mitigaciones

10.1 Riesgos principales

10.1.1 Erosión de boundaries

Riesgo: Sin enforcement automático, boundaries se erosionan con el tiempo.

Síntomas:

  • Imports directos entre módulos (saltando interfaces públicas)
  • Dependencias circulares
  • Shared mutable state

Mitigación:

// Configurar CI para fallar si hay violaciones
// .github/workflows/ci.yml
- name: Architecture Tests
  run: npm run test:arch
  # Debe pasar para mergear PR

// tests/architecture.test.ts
describe('Module Boundaries', () => {
  it('should not have circular dependencies', () => {
    const result = analyzeModuleDependencies('./src/modules');
    expect(result.circularDeps).toHaveLength(0);
  });
});

10.1.2 “God Module”

Riesgo: Un módulo crece demasiado y se convierte en monolito dentro del monolito.

Síntomas:

  • Módulo con >50K LOC
  • > 20 dependencias salientes
  • > 10 equipos tocando el mismo módulo

Mitigación:

  • Métricas de tamaño: Configurar alertas cuando módulo supera umbral
  • Split proactivo: Cuando módulo llega a 30K LOC, planear split
// scripts/check-module-sizes.ts
const modules = getModules("./src/modules");
modules.forEach((module) => {
  const loc = countLOC(module.path);
  if (loc > 30000) {
    console.warn(
      `⚠️  Module ${module.name} has ${loc} LOC. Consider splitting.`,
    );
  }
});

10.1.3 Deployment monolítico

Riesgo: Un bug en un módulo requiere redeploy de todo el sistema.

Mitigación:

  • Feature flags: Desactivar funcionalidad sin redeploy
  • Canary deploys: Deploy gradual con monitoring
  • Rollback rápido: Automatizar rollback (< 5 minutos)
// Ejemplo con LaunchDarkly
if (await featureFlags.isEnabled("new-checkout-flow", user)) {
  return newCheckoutModule.process(cart);
} else {
  return legacyCheckoutModule.process(cart);
}

10.2 Límites del modelo

El monolito modular NO es apropiado cuando:

  1. Escala extrema: >1000 developers, >10M LOC
  • Alternativa: Considerar microservicios o multi-repo modular monoliths
  1. Requisitos de aislamiento: Compliance (PCI-DSS, HIPAA) requiere aislamiento físico
  • Alternativa: Extraer módulos sensibles a servicios separados
  1. Tecnologías heterogéneas: Necesidad de múltiples stacks (Java + Python + Go)
  • Alternativa: Microservicios con contratos bien definidos
  1. Equipos distribuidos globalmente: >100 developers en 10+ zonas horarias
  • Alternativa: Federación de monolitos modulares o microservicios

10.3 Cuándo extraer un módulo a microservicio

Decision matrix:

| Factor | Peso | Umbral | Acción |

| ——————————- | —– | —————————————- | ——————– |

| Escalabilidad independiente | Alta | Necesita 10x más recursos que promedio | Extraer |

| Tecnología diferente | Alta | Requiere stack diferente (ej. Python ML) | Extraer |

| Equipos autónomos | Media | >10 devs trabajando solo en este módulo | Considerar |

| Latency requirements | Media | Sub-10ms crítico | Mantener en monolito |

| Compliance | Alta | Requiere aislamiento físico (PCI) | Extraer |

Proceso de extracción:

1. Verificar que módulo cumple propiedades de monolito modular (Sección 3)
2. Crear servicio nuevo con misma interfaz
3. Implementar Adapter Pattern:
   - Si módulo local: llamada directa
   - Si servicio remoto: llamada HTTP/gRPC
4. Feature flag para switch gradual
5. Monitor latency, errors, throughput
6. Si OK: eliminar código local
7. Si NO: rollback a local

11. Checklist y recomendaciones prácticas

11.1 Checklist de diseño

Estructura de módulos

  • Módulos organizados por dominio (no por capa técnica)
  • Cada módulo tiene estructura consistente (domain, application, infrastructure, api)
  • Shared Kernel mínimo (< 10% del código total)
  • Documentación de boundaries (README.md en cada módulo)

Enforcement de boundaries

  • Architecture tests en CI (ArchUnit, Deptrac, ESLint boundaries)
  • Tests fallan en violaciones (no solo warnings)
  • Pre-commit hooks verifican boundaries localmente
  • CODEOWNERS definido por módulo

Comunicación entre módulos

  • Interfaces explícitas (solo `/api` es público)
  • Domain Events para acoplamiento débil
  • No shared mutable state entre módulos
  • Anti-Corruption Layer para legacy/external systems

Testing

  • Unit tests por módulo (>80% coverage)
  • Integration tests end-to-end (flujos críticos)
  • Architecture tests automatizados
  • Performance tests (latency baselines)

11.2 Recomendaciones por tamaño de equipo

Equipos pequeños (< 10 devs)

  • Estructura: 3-5 módulos principales
  • Enforcement: ESLint + code reviews (no overhead de herramientas pesadas)
  • Comunicación: Direct calls OK (no necesario event bus aún)
  • Deploy: Monolito simple (no necesitas k8s)

Equipos medianos (10-50 devs)

  • Estructura: 10-20 módulos
  • Enforcement: Architecture tests automatizados (ArchUnit/Deptrac)
  • Comunicación: Domain Events (in-memory event bus)
  • Deploy: Containerizado (Docker), orquestación simple
  • Ownership: CODEOWNERS por módulo

Equipos grandes (50-100 devs)

  • Estructura: 20-50 módulos
  • Enforcement: Herramientas dedicadas (Spring Modulith, NDepend)
  • Comunicación: Event-driven architecture (considerar event store)
  • Deploy: Blue-green deployments, canary releases
  • Ownership: Equipos dedicados por módulo, arquitectura team central

11.3 Métricas a monitorear

Métricas de salud arquitectural:

modular_monolith_metrics:
  structural:
    - module_count: 15
    - avg_module_size_loc: 5000
    - max_module_size_loc: 12000 # Alert si > 30K
    - circular_dependencies: 0 # Must be 0
    - avg_coupling: 0.15 # Target < 0.2

  quality:
    - architecture_test_pass_rate: 100% # Must be 100%
    - code_coverage: 85%
    - avg_cyclomatic_complexity: 8

  performance:
    - p50_latency_ms: 15
    - p95_latency_ms: 45
    - throughput_rps: 5000

  operational:
    - deploy_frequency: "10/week"
    - mttr_minutes: 18
    - change_failure_rate: 2%

11.4 Guía de troubleshooting

Problema: Dependencias circulares detectadas

Error: Circular dependency detected: Orders -> Payments -> Invoices -> Orders

Solución:

  1. Identificar la dependencia más débil (generalmente la menos usada)
  2. Introducir Domain Event para romper ciclo:
// Antes: Orders -> Invoices (circular)
class OrderService {
  async createOrder(dto: CreateOrderDTO) {
    const order = new Order(dto);
    await this.invoicesService.createInvoice(order); // CIRCULAR
  }
}

// Después: Orders emite evento, Invoices escucha
class OrderService {
  async createOrder(dto: CreateOrderDTO) {
    const order = new Order(dto);
    await this.eventBus.publish(new OrderPlacedEvent(order));
  }
}

class InvoicesEventHandler {
  @EventHandler(OrderPlacedEvent)
  async onOrderPlaced(event: OrderPlacedEvent) {
    await this.invoicesService.createInvoice(event);
  }
}

Problema: Módulo crece demasiado

Síntomas: Módulo `orders` tiene 50K LOC, 30 developers tocándolo.

Solución: Split en sub-módulos:

modules/orders/  →  modules/orders-placement/
                    modules/orders-fulfillment/
                    modules/orders-returns/

12. Challenge práctico

12.1 Objetivo

Implementar un monolito modular mínimo viable para un e-commerce simplificado.

12.2 Requisitos funcionales

Módulos a implementar:

  1. Orders: Crear, listar órdenes
  2. Payments: Procesar pagos (mock)
  3. Notifications: Enviar emails (mock)

Flujo:

User coloca orden
  → OrdersModule crea orden
  → Emite OrderPlacedEvent
  → PaymentsModule procesa pago
  → Emite PaymentProcessedEvent
  → NotificationsModule envía email confirmación

12.3 Requisitos no-funcionales

  • Estructura: Cada módulo con `/domain`, `/application`, `/api`
  • Enforcement: Architecture tests que verifiquen boundaries
  • Comunicación: Domain Events (in-memory event bus)
  • Testing: Unit tests + integration test end-to-end

12.4 Implementación de referencia (Node.js/TypeScript)

Estructura de proyecto:

modular-ecommerce/
├── src/
│   ├── modules/
│   │   ├── orders/
│   │   │   ├── domain/
│   │   │   │   ├── Order.entity.ts
│   │   │   │   └── OrderPlaced.event.ts
│   │   │   ├── application/
│   │   │   │   ├── CreateOrder.usecase.ts
│   │   │   │   └── OrdersService.ts
│   │   │   └── api/
│   │   │       └── OrdersController.ts
│   │   ├── payments/
│   │   │   ├── domain/
│   │   │   │   └── PaymentProcessed.event.ts
│   │   │   ├── application/
│   │   │   │   ├── ProcessPayment.usecase.ts
│   │   │   │   └── OrderPlacedHandler.ts
│   │   │   └── api/
│   │   │       └── PaymentsService.ts
│   │   ├── notifications/
│   │   │   ├── application/
│   │   │   │   ├── SendEmail.usecase.ts
│   │   │   │   └── PaymentProcessedHandler.ts
│   │   │   └── api/
│   │   │       └── NotificationsService.ts
│   │   └── shared/
│   │       ├── domain/
│   │       │   └── DomainEvent.ts
│   │       └── infrastructure/
│   │           └── EventBus.ts
│   └── tests/
│       ├── architecture.test.ts
│       └── integration.test.ts
├── package.json
└── tsconfig.json

Código clave:

// shared/infrastructure/EventBus.ts
export class EventBus {
  private handlers = new Map<string, Function[]>();

  subscribe(eventName: string, handler: Function) {
    const handlers = this.handlers.get(eventName) || [];
    handlers.push(handler);
    this.handlers.set(eventName, handlers);
  }

  async publish(event: DomainEvent) {
    const handlers = this.handlers.get(event.constructor.name) || [];
    await Promise.all(handlers.map((h) => h(event)));
  }
}

// orders/application/CreateOrder.usecase.ts
export class CreateOrderUseCase {
  constructor(
    private repository: OrderRepository,
    private eventBus: EventBus,
  ) {}

  async execute(dto: CreateOrderDTO): Promise<Order> {
    const order = Order.create(dto);
    await this.repository.save(order);

    await this.eventBus.publish(new OrderPlacedEvent(order.id, order.total));

    return order;
  }
}

// payments/application/OrderPlacedHandler.ts
export class OrderPlacedHandler {
  constructor(
    private processPayment: ProcessPaymentUseCase,
    private eventBus: EventBus,
  ) {
    eventBus.subscribe("OrderPlacedEvent", this.handle.bind(this));
  }

  async handle(event: OrderPlacedEvent) {
    await this.processPayment.execute({
      orderId: event.orderId,
      amount: event.amount,
    });
  }
}

// tests/architecture.test.ts
describe("Module Boundaries", () => {
  it("orders should not import from payments directly", () => {
    const ordersFiles = glob.sync("src/modules/orders/**/*.ts");

    ordersFiles.forEach((file) => {
      const content = fs.readFileSync(file, "utf-8");
      expect(content).not.toMatch(/from ['"].*\/payments\//);
    });
  });
});

12.5 Ejercicio para el lector

Tarea: Añadir módulo `Inventory` que:

  1. Escucha `OrderPlacedEvent`
  2. Verifica stock disponible
  3. Emite `StockReservedEvent` o `StockUnavailableEvent`
  4. `OrdersModule` debe cancelar orden si stock unavailable

Restricciones:

  • No imports directos entre `orders` e `inventory`
  • Architecture tests deben pasar
  • Test de integración debe validar flujo completo

Template de inicio:

Implementar módulo inventory
git clone https://github.com/example/modular-monolith-challenge
npm install
npm test  # Debe pasar todos los tests
npm test  # Debe seguir pasando

13. Conclusión

13.1 Síntesis de hallazgos

Este artículo ha presentado un análisis exhaustivo del monolito modular como arquitectura pragmática para la mayoría de sistemas modernos. Los hallazgos clave incluyen:

  1. Definición formal: Establecimos propiedades matemáticamente verificables que definen un monolito modular, separándolo de monolitos tradicionales
  1. Enforcement práctico: Demostramos que las architecture tests y herramientas automatizadas son esenciales para mantener boundaries, no solo convenciones sociales
  1. Evidencia empírica: Casos de estudio de Segment, Shopify y Basecamp muestran reducciones del 40-78% en latencia, 30-50% mejoras en productividad, y 60-67% en costos operacionales
  1. Experimentos reproducibles: Nuestros benchmarks demuestran que monolitos modulares superan a microservicios en latencia (3.75x más rápidos p50) y throughput (2.7x mayor) para operaciones transaccionales
  1. Guías implementables: Proporcionamos patrones concretos, configuraciones verificables y checklists accionables para múltiples ecosistemas (Java/Spring, Node.js/NestJS, PHP/Laravel)

13.2 Implicaciones para la industria

El renacimiento de los monolitos modulares tiene implicaciones profundas:

Para startups y scale-ups:

El default architecture debería ser monolito modular, no microservicios. Extraer a servicios solo cuando haya evidencia cuantitativa de necesidad (no premature optimization).

Para empresas establecidas:

Considerar consolidación de microservicios en monolitos modulares cuando:

  • Equipos < 100 developers
  • Latencia end-to-end > 200ms (mayoría network overhead)
  • Costos operacionales > 3x del desarrollo

Para la academia:

Más investigación necesaria en:

  • Formalización de propiedades arquitecturales verificables
  • Herramientas de análisis estático cross-language
  • Métricas predictivas de cuándo extraer módulos

13.3 Trabajo futuro

Áreas prometedoras para investigación:

  1. Monolitos modulares distribuidos: Múltiples instancias del monolito con event sourcing compartido
  1. AI-assisted boundary detection: Machine learning para sugerir boundaries óptimos basándose en código existente
  1. Gradual service extraction: Herramientas que automaticen la extracción de módulos a servicios cuando métricas crucen umbrales
  1. Cross-language module systems: Permitir módulos en diferentes lenguajes dentro de un monolito (Wasm?)

13.4 Mensaje final

El objetivo de la arquitectura de software no es elegancia teórica, sino delivery sostenible de valor con complejidad mínima necesaria. Para la gran mayoría de sistemas, el monolito modular logra este balance mejor que alternativas más complejas.

Como dijo Gall’s Law:

> “A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work. You have to start over with a working simple system.”

El monolito modular es ese “simple system that works”, con boundaries que permiten evolución hacia complejidad solo cuando sea necesario.

14. Recursos y referencias

14.1 Libros fundamentales

  1. Evans, Eric. _Domain-Driven Design: Tackling Complexity in the Heart of Software._ Addison-Wesley, 2003.
  • Capítulos clave: Bounded Contexts (Ch. 14), Context Maps (Ch. 15)
  1. Ford, Neal; Parsons, Rebecca; Kua, Patrick. _Building Evolutionary Architectures: Support Constant Change._ O’Reilly, 2017.
  • Focus: Architectural Fitness Functions (Ch. 2)
  1. Newman, Sam. _Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith._ O’Reilly, 2019.
  • Nota: Presenta patrones de migración, pero también cuándo NO migrar
  1. Vernon, Vaughn. _Implementing Domain-Driven Design._ Addison-Wesley, 2013.
  • Capítulos: Modules (Ch. 9), Aggregates (Ch. 10)

14.2 Artículos académicos

  1. Knoche, Holger; Hasselbring, Wilhelm. “Using Microservices for Legacy Software Modernization.” _IEEE Software_, vol. 35, no. 3, 2018, pp. 44-49.
  • DOI: 10.1109/MS.2018.2141026
  1. Taibi, Davide; Lenarduzzi, Valentina; Pahl, Claus. “Architectural Patterns for Microservices: A Systematic Mapping Study.” _CLOSER 2018_, 2018.
  • Incluye análisis comparativo de complejidad
  1. Shadija, Dharmendra; Rezai, Mo; Hill, Richard. “Towards an Understanding of Microservices.” _International Conference on Advanced Computer Science and Information Systems_, 2017.

14.3 Blogs y recursos de la industria

Shopify Engineering:

  • “Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity”
  • https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity

Segment Engineering:

  • “Goodbye Microservices: From 100s of problem children to 1 superstar”
  • https://segment.com/blog/goodbye-microservices/

Martin Fowler’s Blog:

  • “MonolithFirst” – https://martinfowler.com/bliki/MonolithFirst.html
  • “BoundedContext” – https://martinfowler.com/bliki/BoundedContext.html

37signals (DHH):

  • “The Majestic Monolith” – https://m.signalvnoise.com/the-majestic-monolith/

Kamil Grzybek (Modular Monolith):

  • Blog: https://www.kamilgrzybek.com/
  • GitHub: https://github.com/kgrzybek/modular-monolith-with-ddd

14.4 Herramientas

Architecture Testing:

  • ArchUnit (Java): https://www.archunit.org/
  • NDepend (.NET): https://www.ndepend.com/
  • Deptrac (PHP): https://github.com/qossmic/deptrac
  • ESLint plugin boundaries (JS/TS): https://github.com/javierbrea/eslint-plugin-boundaries

Frameworks:

  • Spring Modulith: https://spring.io/projects/spring-modulith
  • Packwerk (Ruby): https://github.com/Shopify/packwerk
  • NestJS Modules: https://docs.nestjs.com/modules

Analysis Tools:

  • Madge (JS circular deps): https://github.com/pahen/madge
  • PhpMetrics: https://www.phpmetrics.org/
  • Structure101: https://structure101.com/

14.5 Cursos y tutoriales

Pluralsight:

  • “Clean Architecture: Patterns, Practices, and Principles” – Matthew Renze

Udemy:

  • “Domain-Driven Design: Complete Software Architecture Course”

YouTube:

  • CodeOpinion channel: https://www.youtube.com/c/CodeOpinion
  • Series “Modular Monoliths” (12 videos)

14.6 Repositorios de referencia

  1. Modular Monolith with DDD (.NET)
  • https://github.com/kgrzybek/modular-monolith-with-ddd
  • Incluye: ArchUnit tests, Event Bus, CQRS
  1. NestJS Modular Monolith Example
  • https://github.com/nestjs/nest/tree/master/sample/23-modules
  • Ejemplo oficial de NestJS team
  1. Laravel Modular
  • https://github.com/nwidart/laravel-modules
  • Package para estructura modular en Laravel
  1. Modular Monolith Java (Spring Boot)
  • https://github.com/buckpal/buckpal
  • Clean Architecture + Hexagonal + Modular

14.7 Comunidades y foros

  • Domain-Driven Design Community: https://dddcommunity.org/
  • Software Architecture Reddit: r/softwarearchitecture
  • DDD/CQRS/ES Slack: https://ddd-cqrs-es.herokuapp.com/

Apéndice: Fragmentos de configuración

A.1 ArchUnit configuration (Java)

// src/test/java/com/example/ArchitectureTest.java
package com.example;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;

public class ArchitectureTest {
    private final JavaClasses classes = new ClassFileImporter()
        .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
        .importPackages("com.example");

    @Test
    public void modules_should_not_have_circular_dependencies() {
        ArchRule rule = slices()
            .matching("com.example.modules.(*)..")
            .should().beFreeOfCycles();

        rule.check(classes);
    }

    @Test
    public void domain_should_not_depend_on_application() {
        ArchRule rule = noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat()
            .resideInAPackage("..application..");

        rule.check(classes);
    }

    @Test
    public void only_api_package_should_be_public() {
        ArchRule rule = classes()
            .that().resideInAPackage("..internal..")
            .should().notBePublic();

        rule.check(classes);
    }

    @Test
    public void layered_architecture_should_be_respected() {
        layeredArchitecture()
            .layer("API").definedBy("..api..")
            .layer("Application").definedBy("..application..")
            .layer("Domain").definedBy("..domain..")
            .layer("Infrastructure").definedBy("..infrastructure..")
            .whereLayer("API").mayNotBeAccessedByAnyLayer()
            .whereLayer("Application").mayOnlyBeAccessedByLayers("API")
            .whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
            .check(classes);
    }
}

A.2 Deptrac configuration (PHP)

deptrac.yaml
parameters:
  paths:
    - ./src
  exclude_files:
    - "#.*test.*#"
  layers:
    - name: API
      collectors:
        - type: className
          regex: .*\\Api\\.*
    - name: Application
      collectors:
        - type: className
          regex: .*\\Application\\.*
    - name: Domain
      collectors:
        - type: className
          regex: .*\\Domain\\.*
    - name: Infrastructure
      collectors:
        - type: className
          regex: .*\\Infrastructure\\.*
  ruleset:
    API:
      - Application
      - Domain
    Application:
      - Domain
    Domain: []
    Infrastructure:
      - Domain
      - Application
  skip_violations:
    # Lista de violaciones existentes que se permitirán temporalmente
    # (vacío en greenfield project)

A.3 ESLint boundaries (TypeScript)

// .eslintrc.js
module.exports = {
  parser: "@typescript-eslint/parser",
  plugins: ["@typescript-eslint", "boundaries"],
  extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  settings: {
    "boundaries/elements": [
      { type: "shared", pattern: "src/modules/shared/*" },
      { type: "orders", pattern: "src/modules/orders/*" },
      { type: "payments", pattern: "src/modules/payments/*" },
      { type: "notifications", pattern: "src/modules/notifications/*" },
    ],
    "boundaries/ignore": ["**/*.test.ts", "**/*.spec.ts"],
  },
  rules: {
    "boundaries/element-types": [
      "error",
      {
        default: "disallow",
        rules: [
          {
            from: ["orders"],
            allow: ["shared", "orders"],
          },
          {
            from: ["payments"],
            allow: ["shared", "payments"],
          },
          {
            from: ["notifications"],
            allow: ["shared", "notifications"],
          },
          {
            from: ["*"],
            disallow: ["**/internal/**"],
            message: "Internal modules should not be imported directly",
          },
        ],
      },
    ],
  },
};

A.4 Docker Compose para desarrollo local

docker-compose.yml
version: "3.8"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - ./src:/app/src
      - /app/node_modules
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:pass@postgres:5432/modular_monolith
      - REDIS_URL=redis://redis:6379
    depends_on:
      - postgres
      - redis
    command: npm run dev

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=modular_monolith
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  pgadmin:
    image: dpage/pgadmin4
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@example.com
      - PGADMIN_DEFAULT_PASSWORD=admin
    ports:
      - "5050:80"
    depends_on:
      - postgres

volumes:
  postgres_data:

A.5 GitHub Actions CI pipeline

.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  architecture-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run architecture tests
        run: npm run test:arch

      - name: Check module boundaries
        run: npm run lint:boundaries

  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm run test:unit -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3

  integration-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15-alpine
        env:
          POSTGRES_USER: user
          POSTGRES_PASSWORD: pass
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run integration tests
        env:
          DATABASE_URL: postgresql://user:pass@localhost:5432/test_db
        run: npm run test:integration

  build:
    runs-on: ubuntu-latest
    needs: [architecture-tests, unit-tests, integration-tests]
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: dist/

Deja un comentario

Scroll al inicio

Discover more from Creapolis

Subscribe now to keep reading and get access to the full archive.

Continue reading