# Por qué probablemente no deberías usar Microservicios (Sin embargo)

<!--category-- Architecture, Microservices, Opinion -->
<datetime class="hidden">2025-12-29T20:00</datetime>

Los microservicios se han convertido en la arquitectura predeterminada del "sistema serio".

Si quieres sonar maduro, hablas de mallas de servicio, autobuses de eventos, rastreo distribuido y "desplegabilidad independiente". Si quieres lucir listo para la empresa, dibujas cajas hasta que el diagrama parece un tazón de espaguetis que alguien lanzó a una pizarra blanca.

Pero aquí está el correctivo de la mayoría de los jóvenes no se les da:

**Los microservicios no son una actualización de la arquitectura de aplicaciones, sino una estrategia de escalamiento organizacional.**

Si no tienes ya el dolor organizativo que resuelven, adoptarlos temprano no te hace a prueba de futuro. Te hace presente roto.

Esto no es un argumento teórico. Es un intercambio de ingeniería, y como todos los intercambios, sólo tiene sentido cuando entiendes el impuesto que estás pagando y lo que estás recibiendo a cambio.

He sido tan culpable como cualquiera de "hacer microservicios" sin internalizar completamente los gastos generales de organización que implican. Es fácil quedar atrapado en el vocabulario y olvidar que el verdadero desafío es gestionar la complejidad entre equipos y sistemas.

Al final se reduce al patrón más antiguo en ingeniería de software: **KISS** - mantenlo sencillo, estúpido.

[TOC]

## El mito de los microservicios

Los microservicios se venden como arquitectura predeterminada porque la narrativa es seductora:

* Los servicios pequeños son "más limpios"
* Los sistemas distribuidos son "modernos"
* La autonomía es "libre"
* Puedes "escalar más tarde" porque empezaste "bien"

Este encuadre está al revés.

Los microservicios no son principalmente sobre la estructura de código. **quién puede cambiar qué, sin hablar con quién, y con qué frecuencia**.

Si no tienes:

* Múltiples equipos de envío de forma independiente
* Prioridades conflictivas
* Obstáculos de coordinación
* Alegaciones relativas al despliegue
* Límites de la propiedad real

...entonces no estás resolviendo un problema de organización.

La arquitectura se trata en última instancia de habilitar a la gente, no mover cajas alrededor.

## El diagrama que debería ser una etiqueta de advertencia

Has visto este diagrama.

![Un diagrama de arquitectura de microservicios con precaución](bad_microservices.png?format=webp&height=600)

Esta no es una "arquitectura de ejemplo para copiar", es una etiqueta de advertencia.

Los cursos a menudo enseñan microservicios como diagramas porque los diagramas son más fáciles que enseñar responsabilidad operacional.

El reframing importante es este: que el diagrama no es un estado objetivo. Es un **superficie de costes**.

Cada caja es un compromiso:

* Tiempo de funcionamiento
* Un despliegue para gestionar
* Un contrato a la versión
* Un modo de fallo que ahora posees
* Una historia de guardia que no puedes ignorar

Las flechas se ven impresionantes, también esconden la realidad.

Porque cada flecha es:

* Latencia
* Fallo parcial
* Retenes
* Tiempos de espera
* Rastrear la propagación del contexto
* "Trabaja en la puesta en escena" mentiras

Si una arquitectura requiere esto en el primer día, usted no está construyendo un producto. Usted está construyendo una plataforma.

## El modelo de costos ocultos de los microservicios

Como todas las decisiones arquitectónicas, los microservicios vienen con un **impuesto sobre la complejidad** (véase [Jugar con el código: impuesto de complejidad](/blog/playing-with-code-efficient-agile). La diferencia es que este impuesto se compone a través de cada límite de servicio, cada despliegue, y cada modo de fallo.

Los costos rara vez son explícitos, por lo general se disfrazan de "mejores prácticas".

### Gastos generales de funcionamiento

Cada servicio quiere:

* Su propia tubería de construcción
* Su propia configuración de despliegue
* Su propio seguimiento y alerta
* Su propio registro, tableros, runbooks
* Su propia postura de seguridad (secretos, aut, entrada)

Si un equipo no puede hacer esto cómodamente, no tiene microservicios. **un monolito distribuido con pasos adicionales**.

### Multiplicación de latencia y fallo

En un monolito, una llamada es una llamada de función.

En los microservicios, una "llamada" es una tregua negociada entre:

* Redes
* Equilibrios de carga
* TLS
* Auth middleware
* Serialización
* Tiempos de espera
* Retenes
* Retraso en la presión

Los modos de fallo se multiplican:

* Uno río abajo es lento → los hilos aguas arriba se acumulan
* Retries amplifican la carga → que DDoS usted mismo educadamente
* Un solo colgajo de dependencia → tres servicios degradan "aleatoriamente"

Ya no depuras bichos. **Estados del sistema**.

### El impuesto a la serialización de que nadie habla

Este es un costo que casi nunca se discute en la defensa de microservicios: **gastos generales de serialización y desarealización (SerDe)**.

Cada límite de servicio significa:

```csharp
// Monolith: direct object reference
var user = _userService.GetUser(userId);  // < 1μs
var tier = user.Tier;                      // Memory access

// Microservices: serialize → network → deserialize
var json = JsonSerializer.Serialize(user);        // ~50μs for a complex object
var bytes = Encoding.UTF8.GetBytes(json);         // ~10μs
// ... send over network ...
var responseJson = await response.Content.ReadAsStringAsync();  // ~20μs
var user = JsonSerializer.Deserialize<User>(responseJson);      // ~70μs
var tier = user.Tier;                                           // Finally
```

**Por llamada, eso es ~150μs de carga de CPU pura antes de que la red se involucre.**

Ahora multiplíquelo a través de una cadena de llamadas:

* El servicio de pedido deserializa la solicitud (150μs)
* Servicio de pedido serializa Solicitud de servicio de usuario (150μs)
* El servicio al usuario deserializa la solicitud (150μs)
* El servicio de usuario serializa la respuesta (150μs)
* El servicio de pedido deserializa la respuesta del usuario (150μs)
* Servicio de pedidos serializa solicitud de inventario (150μs)
* y así sucesivamente.

**En una cadena de 5 servicios, usted está gastando ~1.5ms sólo en SerDe** Si estás usando XML, Protocol Buffers con reflexión, o serializadores ineficientes, multiplíquelo por 2-10x.

Sí, puedes reducir esto con técnicas como [Generación de fuentes JSON en .NET](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation):

```csharp
[JsonSerializable(typeof(User))]
internal partial class UserJsonContext : JsonSerializerContext { }

// ~30-40% faster than reflection-based serialization
var json = JsonSerializer.Serialize(user, UserJsonContext.Default.User);
```

Pero sigues haciendo serialización, acabas de hacer el impuesto un poco más barato, en un monolito, el impuesto es cero.

#### Un ejemplo real: El desastre de Serde

Una vez perfilé un sistema de búsqueda de direcciones con esta arquitectura (todos en contenedores de Kubernetes):

```
ASP.NET API → Go Service (fan-out) → ASP.NET Search Service → Elasticsearch
```

El servicio Go manejó el fan-out a múltiples fuentes de datos y resultados agregados. Cada solicitud significó múltiples saltos de serialización a través de los límites del contenedor.

**El desglose de una búsqueda de direcciones típica:**

* API ASP.NET: Serializar solicitud de búsqueda → JSON (80μs)
* Red: ASP.NET → Ir (varía por carga de racimo)
* Servicio Go: Solicitud de deserialización (40μs)
* Go Service: Fan out - serializar peticiones a múltiples motores (60μs × N backends)
* Red: Ir → Búsqueda ASP.NET (varias)
* Servicio de búsqueda: Deserialize request (90μs)
* Servicio de búsqueda: Construir consulta Elasticsearch, serializar (120μs)
* Red: Buscar → Elasticsearch (varios)
* Elasticsearch: Deserializar consulta, ejecutar, serializar resultados (búsqueda real ~5ms, SerDe ~200μs)
* Red: Elasticsearch → Buscar (varios)
* Servicio de búsqueda: Deserialize ES response (300μs), map to domain model, serialize (180μs)
* Red: Buscar → Ir (varios)
* Go Service: Deserializar las respuestas de todos los backends (150μs × N), agregado, serializar (80μs)
* Red: Go → ASP.NET (varios)
* API de ASP.NET: Deserializar la respuesta final (120μs)

**Para un ventilador de 3-backend:**

* Total de los gastos generales de SerDe: ~2ms antes de que Elasticsearch empezara
* Redes aéreas entre contenedores: ~2-3ms
* Búsqueda real + lógica de negocio: ~6ms

**Pasamos casi tanto tiempo en SerDe como en la búsqueda real.**

El costo no era el servicio Go en sí mismo - fan-out tenía sentido para el caso de uso. **límites de servicio**. Cada borde de contenedor significa serializar → red → deserializar.

En un monolito haciendo la misma lógica de fan-out:

* Mismas consultas de Elasticsearch
* La misma lógica de agregación
* Zero SerDe interservicio (sólo llamadas al método)
* ~40% menos latencia total

Este costo:

* Escalas con complejidad de objeto (objetos anidados, colecciones, polimorfismo)
* Compuestos con profundidad de llamada (cadenas de servicio)
* Quema la CPU en cada petición (no se puede guardar en caché)
* Aumenta la presión GC (allocation churn from string/byte arrays)
* **Suma rápido cuando estás haciendo cientos de peticiones por segundo**

En un monolito, este costo es cero. El objeto ya está en memoria.

### El mito del rendimiento: rendimiento vs. escalabilidad

He aquí un argumento de ventas común: "Los microservicios mejoran el rendimiento".

**Esto es al revés.**

Los microservicios son **más lento** Vamos a ser precisos sobre lo que queremos decir:

**Producción** (solicitudes por segundo por núcleo de CPU):

- **Monolito**: Superior - cero SerDe, cero lúpulo de red, llamadas de método directo
- **Microservicios**: Lower - cabeza de SerDe, latencia de la red, orquestación de contenedores

**Escalabilidad** (capacidad para atender más solicitudes totales mediante la adición de recursos):

- **Monolito**: Limitado por escala vertical (máquinas más grandes)
- **Microservicios**: Horizontal - añadir más instancias de servicios de cuello de botella

Los microservicios le permiten **escala independientemente**Si tu servicio de inventario necesita recursos de 10x pero tu servicio de usuario no, puedes escalarlos por separado. **cuando lo necesitas**.

Pero no es una optimización de rendimiento. **estrategia de escalamiento**Y viene con un impuesto.

#### Los monolitos modernos también pueden escalarse

El argumento "microservicios escala mejor" tuvo más sentido en 2010 cuando los servidores tenían 4-8 núcleos.

Los servidores modernos tienen 32-128 núcleos. Una sola máquina puede ejecutar miles de operaciones simultáneas de manera eficiente.

Si su monolito está construido con modernos patrones de ejecución concurrente, puede manejar el rendimiento masivo en una sola implementación. Por ejemplo, usando patrones como [Coordinadores de ejecución efímera](/blog/ephemeral-execution-library), puedes:

```csharp
// Process thousands of concurrent operations in a monolith
services.AddEphemeralWorkCoordinator<TranslationRequest>(
    async (request, ct) => await TranslateAsync(request, ct),
    new EphemeralOptions { MaxConcurrency = Environment.ProcessorCount * 4 });

// Bounded, observable, efficient concurrent processing
// No network overhead, no SerDe, no container orchestration
// Can handle 10k+ req/sec on a single machine
```

Esto le da:

- **Paralelismo**: El trabajo corre simultáneamente en todos los núcleos
- **Observabilidad**: Seguimiento de las operaciones pendientes/activas/fallas
- **Retraso en la presión**: Las colas fijas evitan el agotamiento de la memoria
- **Cero gastos generales**: Sin serialización, sin red, sin rastreo distribuido

Cuando tú **@info: tooltip** necesidad de escalar más allá de una máquina, usted puede:

1. Ejecutar múltiples instancias detrás de un balanceador de carga (escalado horizontal sin estado)
2. Utilice un patrón de salida para una distribución de trabajo asíncrona
3. Partición por clave (identificación del cliente, región, etc.) en todas las instancias

Sigues ejecutando un monolito, lo has desplegado varias veces.

**El punto**: No se divida en microservicios para "rendimiento". **escalado independiente de componentes específicos** justifica los gastos generales de funcionamiento.

### Carga cognitiva

Los microservicios no reducen la complejidad, sino que lo redistribuyen.

El "servicio simple" todavía necesita contexto:

* ¿Quién lo llama?
* ¿Qué significa "corregir"?
* ¿Qué pasa cuando está caído?
* ¿Cuál es el plan de retroceso?
* ¿Cuál es el gráfico de dependencia?

La gente subestima esto porque los diagramas lo ocultan.

### Versión y deriva de contrato

Cada límite se convierte en un contrato.  
Cada contrato se convierte en:

* Normas de versión
* Garantías de compatibilidad
* Limitaciones de la evolución del esquema
* Coordinación por encima de lo que fingiste que no tenías

Usted puede esquivar esto por un tiempo con "sólo desplegar todo juntos".

Ese es el chiste: si despliegas todo juntos, construyes un monolito, sólo uno peor.

### Herramientas antes del valor

El gasto temprano es siempre el mismo:

* Descubrimiento de servicios
* Gestión de secretos
* Rastreo distribuido
* Tala centralizada
* Métricas y alertas
* Plantillas CI/CD
* Historia local de desarrollo que no es dolorosa
* Gestión de las dependencias
* Una manera de correr todo sin llorar

Ninguna de estas naves valor de producto.

Y la mayoría de los equipos lo hacen antes de demostrar que el producto merece la complejidad.

Este es el error central: **Usted paga el impuesto de la ceremonia antes de haber ganado el valor**Como [standups diarios que pierden tiempo sin ofrecer alineación](/blog/agile-standups-ceremony-tax), los microservicios pueden convertirse en arquitectura ritual: impresionante de ver, caro de mantener y desconectado del problema real que estás resolviendo.

Tomados en conjunto, estos costos no desaparecen - se agravan. Y se agravan si la organización necesita o no microservicios en primer lugar.

## Lo que la gente olvida: Humanos y equipos

La arquitectura existe para ayudar a grupos de humanos a cambiar el software de forma segura.

No para impresionar a otros ingenieros.  
No satisfacer un diagrama.  
No para justificar un equipo de plataformas que no tienes.

El tamaño del equipo y la cadencia de despliegue importan más que el catálogo de patrones.

* Si un equipo posee toda la hoja de ruta, un monolito suele ser más rápido y seguro.
* Si aún comprende el sistema de extremo a extremo, los microservicios reducirán esa claridad.
* Si no tiene límites de propiedad estables, los microservicios forzarán la coordinación a través de APIs, que es el mecanismo de coordinación más lento disponible.

**La ley de Conway no es opcional, es física.**

Tu arquitectura reflejará tu estructura de comunicación te guste o no.

Los microservicios no crean autonomía, lo requieren.

## En defensa del Monolito Modular

La mayoría de los sistemas deberían empezar como monolito, pero no como descuidado.

A **monolito modular** es:

* Una unidad de despliegue
* Un tiempo de ejecución
* Un lugar para depurar
* Una visión coherente del estado
* Pero con límites internos reales

Significa:

* Módulos de dominio con dependencias explícitas
* Borrar la propiedad en código
* Nada de "todo hace referencia a todo"
* Contratos aplicados dentro de la base de códigos

El punto no es permanecer monolítico para siempre. **ganar límites antes de hacerlos remotos**.

Un monolito modular te obliga a aprender las habilidades que los microservicios pretenden resolver:

* Modelización de dominios (límites reales, no límites de carpetas)
* Separación de preocupaciones (no "hemos hecho un servicio")
* Probabilidad
* Cambiar la disciplina
* Saber lo que su sistema realmente hace

**Si no puedes construir un monolito modular limpio, los microservicios no te salvarán.**

### Buen diseño monolítico: Patrones que escalan más tarde

El diseño monolito correcto le permite **aplazar la decisión sobre los microservicios** sin encerrarte.

#### El patrón Outbox: Async sin distribución

Uno de los mejores ejemplos es el **patrón de salida** - una manera de lograr la consistencia eventual y el procesamiento sincronizado *interior* un monolito, con un camino limpio a la distribución más tarde.

En lugar de:

```csharp
// Tightly coupled synchronous code
public async Task PlaceOrder(Order order)
{
    await _orderRepo.Save(order);
    await _emailService.SendConfirmation(order);  // Blocks on external service
    await _inventoryService.Reserve(order.Items); // Blocks on another service
}
```

Escribes:

```csharp
// Outbox pattern: write events to a table
public async Task PlaceOrder(Order order)
{
    await using var transaction = await _db.Database.BeginTransactionAsync();
    
    // Save order
    await _orderRepo.Save(order);
    
    // Write events to outbox table (same transaction)
    _db.OutboxMessages.Add(new OutboxMessage
    {
        EventType = "OrderPlaced",
        Payload = JsonSerializer.Serialize(order),
        CreatedAt = DateTime.UtcNow
    });
    
    await _db.SaveChangesAsync();
    await transaction.CommitAsync();
    
    // Background worker picks up outbox events and publishes them
}
```

**Lo que esto te da:**

* **Coherencia de las transacciones**: Los eventos y los datos se comprometen juntos o no en absoluto
* **Desacoplamiento**: La lógica de email/inventory ejecuta async, no bloquea la creación de orden
* **Fiabilidad**: Los acontecimientos persisten incluso si los sistemas aguas abajo están caídos
* **Vía de extracción limpia**: Más tarde, cambiar el trabajador de la bandeja de salida por Kafka/RabbitMQ sin cambiar la lógica de orden

Los [Proyecto de muestra de SegmentCommerce](/blog/zero-pii-customer-intelligence-part2) demuestra este patrón en la producción:

```csharp
// Mostlylucid.SegmentCommerce/Services/Queue/PostgresOutbox.cs
public class PostgresOutbox
{
    public async Task PublishAsync<T>(string eventType, T payload)
    {
        var message = new OutboxMessage
        {
            EventType = eventType,
            Payload = JsonSerializer.Serialize(payload),
            CreatedAt = DateTime.UtcNow
        };
        
        _db.OutboxMessages.Add(message);
        // Caller commits transaction
    }
}

// Background service
public class OutboxProcessor : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var pending = await _db.OutboxMessages
                .Where(m => !m.Processed)
                .OrderBy(m => m.CreatedAt)
                .Take(100)
                .ToListAsync();
                
            foreach (var message in pending)
            {
                await ProcessMessage(message);
                message.Processed = true;
            }
            
            await _db.SaveChangesAsync();
            await Task.Delay(1000, stoppingToken);
        }
    }
}
```

Esto se ejecuta en una sola aplicación hoy. Cuando necesitas escalar:

1. Reemplazar el trabajador de fondo por un consumidor de buses de mensajes
2. Publique eventos de outbox a Kafka/RabbitMQ en lugar de procesar localmente
3. **El código de colocación del pedido no cambia**

Usted ha construido infraestructura lista para microservicios sin pagar el impuesto de distribución.

#### Otros patrones que vale la pena hacer temprano

* **CQRS (luz)**: Separar modelos de lectura/escritura incluso si comparten una DB
* **Aprovisionamiento de eventos (selectivos)**: Solo para las entidades de auditoría crítica
* **Banderas de características**: Desacoplamiento de la liberación
* **Trabajos de fondo**: No bloquee peticiones, procese async

Ninguno de estos requisitos requiere distribución. Todos ellos hacen la distribución más fácil más adelante.

## Cuando los microservicios realmente hacen sentido

Los microservicios tienen sentido cuando las limitaciones son reales, no aspirativas.

La pregunta crítica no es "¿deberíamos usar microservicios?" **¿Los beneficios superan el impuesto operativo?**

Como elegir entre [pequeños modelos locales y LLMs fronterizos](/blog/small-models-not-budget-option), se trata de entender dónde pertenece la complejidad y qué modos de fallo puede permitirse.

### Los buenos disparadores

* **La contienda del equipo es mensurable**: Los equipos se bloquean unos a otros semanalmente
* **Se necesitan despliegues independientes**: Cadencia de liberación difiere por dominio
* **La escala es desigual**: Un subsistema necesita 10 recursos y aislamiento
* **Cuestiones de aislamiento en caso de fracaso**: Un componente no debe quitar el resto
* **El aislamiento reglamentario/de datos es obligatorio**: Los límites no son negociables
* **Org estructura ya es multi-equipo**: La propiedad existe y es estable

### Activadores malos

* "Podríamos escalar"
* "Netflix lo hace"
* "Mejor práctica"
* "Se verá bien"
* "Nuestro monolito está desordenado, así que lo arreglaremos dividiéndolo"

**Si tu razón contiene las palabras *puede*, *con el tiempo*, o *a prueba de futuro*, probablemente no sea una razón.**

## La realidad técnica: lo que cuestan los microservicios

Esto es lo que cambia cuando divides un monolito en servicios.

### Cadenas de llamadas y latencia

**Monolito:**

```csharp
public async Task<Order> PlaceOrder(OrderRequest request)
{
    var user = await _userService.GetUser(request.UserId);
    var inventory = await _inventoryService.CheckStock(request.Items);
    var price = _pricingService.Calculate(request.Items, user.Tier);
    
    var order = new Order { /* ... */ };
    await _orderRepository.Save(order);
    await _emailService.SendConfirmation(order);
    
    return order;
}
```

Latencia total: ~50ms (llamadas en proceso + 2 consultas DB)

**Microservicios:**

```csharp
public async Task<Order> PlaceOrder(OrderRequest request)
{
    // HTTP call to User Service (network + TLS + serialization)
    var user = await _httpClient.GetAsync<User>($"http://user-service/users/{request.UserId}");
    
    // HTTP call to Inventory Service
    var inventory = await _httpClient.PostAsync<StockResult>(
        "http://inventory-service/check", request.Items);
    
    // HTTP call to Pricing Service
    var price = await _httpClient.PostAsync<PriceResult>(
        "http://pricing-service/calculate", 
        new { items = request.Items, tier = user.Tier });
    
    // HTTP call to Order Service
    var order = await _httpClient.PostAsync<Order>(
        "http://order-service/orders", request);
    
    // Event published to message bus
    await _messageBus.Publish(new OrderPlaced { OrderId = order.Id });
    
    return order;
}
```

Latencia total: ~400ms (incluso optimista 50–80ms por llamada HTTP en entornos reales + bus de mensajes publicar ~20ms)

**Lo que ganaste:**

* Cada servicio puede implementarse de forma independiente
* Inventario y precios pueden escalar por separado de los pedidos
* Fallo en el correo electrónico no bloquea la creación de orden (evento de sincronización)

**Lo que pagaste:**

* ~8x aumento de latencia
* 4 nuevos puntos de fallo (cualquier servicio puede bajar/bajar)
* Red, DNS, TLS en cada llamada
* **Gastos generales de serialización/deserialización** (ver sección anterior)
* Necesidad de lógica de reintentar, interruptores, tiempos de espera
* Rastreo distribuido a fallos de depuración

### Error al manejar la explosión

**Manejo de errores de Monolito:**

```csharp
try
{
    var order = await PlaceOrder(request);
    return Ok(order);
}
catch (InsufficientStockException)
{
    return BadRequest(new { error = "Out of stock" });
}
catch (Exception ex)
{
    _logger.LogError(ex, "Order placement failed");
    return StatusCode(500);
}
```

**Manejo de errores de microservicios:**

```csharp
try
{
    // Call User Service
    var user = await _httpClient.GetAsync<User>($"http://user-service/users/{request.UserId}");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    return NotFound(new { error = "User not found" });
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.ServiceUnavailable)
{
    // Retry with exponential backoff?
    // Circuit breaker opened?
    // Fail fast or degrade gracefully?
    _logger.LogWarning("User service unavailable, retrying...");
    await Task.Delay(TimeSpan.FromMilliseconds(100));
    // ... retry logic ...
}
catch (TaskCanceledException ex)
{
    // Timeout - was the request processed? Do we retry?
    _logger.LogError("User service timeout");
    return StatusCode(503, new { error = "Service temporarily unavailable" });
}

try
{
    var inventory = await _httpClient.PostAsync<StockResult>(
        "http://inventory-service/check", request.Items);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.BadRequest)
{
    // Business error from remote service
    var errorDetails = await ex.Content.ReadAsAsync<ErrorResponse>();
    return BadRequest(new { error = errorDetails.Message });
}
catch (HttpRequestException ex)
{
    // Which service failed? Network issue? Service down?
    // Do we have a fallback? Cached data? Fail fast?
    _logger.LogError(ex, "Inventory service failed");
    // Maybe try a backup instance?
    // ... more retry logic ...
}

// And repeat for every service call...
```

Cada llamada de red introduce:

* Hipótesis de tiempo de espera
* Fallos de la red
* No disponibilidad del servicio
* Fracasos parciales (solicitud enviada, respuesta no recibida)
* Amplificación de reintento (su reintento podría desencadenar su reintento)
* Las pesadillas de las transacciones distribuidas

### Coordinación del despliegue

**Despliegue de Monolito:**

```bash
# Build
dotnet publish -c Release

# Run migrations
dotnet ef database update

# Deploy
docker push myapp:v1.2.3
kubectl set image deployment/myapp myapp=myapp:v1.2.3

# Rollback if needed
kubectl rollout undo deployment/myapp
```

**Despliegue de microservicios:**

Estás cambiando la estructura del pedido. `Order` incluye `UserTier` directamente (desnormalizado para el rendimiento).

```bash
# 1. Deploy Pricing Service v2 (understands both old and new Order format)
kubectl set image deployment/pricing-service pricing-service=pricing:v2.0.0

# 2. Wait for rollout and monitor
kubectl rollout status deployment/pricing-service
# Check metrics - is v2 working with old order format?

# 3. Deploy Order Service v2 (starts sending new format)
kubectl set image deployment/order-service order-service=order:v2.0.0

# 4. Monitor for errors
# If Pricing v2 has a bug, you can't just rollback Order service
# You have to rollback both in reverse order

# 5. Deploy User Service v2 (stops including tier in response)
# But wait - old Order Service instances might still be running!
# Need zero-downtime rollout or maintain backward compat

# 6. Eventually clean up old code paths in Pricing Service v3
# (6 months later because you're scared to remove backward compat)
```

Con los microservicios, cada cambio cruza múltiples servicios. La evolución del contrato requiere:

* Ventanas de compatibilidad hacia atrás
* Banderas de características en todos los servicios
* Implementación coordinada
* Matrices de prueba extendidas (Servicio A v1 + Servicio B v2, Servicio A v2 + Servicio B v1, etc.)

Nada de esto es imposible, pero requiere disciplina, herramientas y experiencia que la mayoría de los equipos sólo ganan después de años de dolor.

### La experiencia de depuración

**Informe de fallo de Monolith:** "La colocación de pedidos falla para los usuarios premium"

```csharp
// Set breakpoint in PlaceOrder
// Step through each call
// User tier is null - ah, there's the bug
// Fix, test, deploy
```

**Informe de fallo de Microservices:** "La colocación de pedidos falla para los usuarios premium"

```bash
# 1. Check logs - which service failed?
kubectl logs -l app=order-service --tail=100

# 2. Oh, it's calling Pricing service. Check those logs
kubectl logs -l app=pricing-service --tail=100

# 3. Grep for correlation ID across all services
stern -l app=order-service,app=pricing-service,app=user-service \
  | grep "correlation-id-xyz"

# 4. Reconstruct the call chain from distributed traces
# Open Jaeger/Zipkin, find the trace
# User service returned 200 OK
# Pricing service returned 400 Bad Request
# Error: "user.tier is required"

# 5. Check User service - did it send tier?
# Look at schema version - ah, User v1.2 stopped sending tier
# When did that deploy? Was Pricing service updated?

# 6. Check API contracts
# Pricing expects tier, User stopped sending it
# Who approved this change?

# 7. Fix requires coordinating two teams
# Can't just deploy a fix - need contract negotiation
```

Esta es la realidad: **Los errores que fueron correcciones de 5 minutos se convierten en investigaciones multi-equipo**.

Si esto suena extremo, bueno, los microservicios son ingeniería extrema.

## Cómo evolucionar con seguridad: Monolito → Microservicios

Los microservicios son una puerta de un solo sentido.

El camino seguro parece aburrido:

### 1. Probar los límites primero

Si no puedes dibujar el límite dentro del monolito (con dependencias ejecutables), no puedes extraerlo con seguridad.

Obtener las costuras justo a nivel local primero.

### 2. El extracto lee antes de escribir

Las lecturas son más fáciles:

* Menos invariantes
* Menos requisitos de coherencia
* Menos pesadillas de retroceso

Mover modelos leídos hacia fuera primero si usted necesita separación.

### 3. Prefiere Async Antes de Sincronizar

Las llamadas de servicio sincronizadas crean cadenas de dependencia.

La mensajería de Async crea:

* Buffering
* Resiliencia
* Desacoplamiento que en realidad puede sobrevivir

Comience separando eventos y hechos, no cortando puntos finales.

### 4. Estrangular, no volver a escribir

No es una gran explosión.  
No "lo reescribiremos correctamente".

Cortar un límite, medirlo, poseerlo, y seguir adelante.

**Si no puedes explicar por qué un servicio existe independientemente, no debería existir en absoluto.**

## La comida para llevar: Los microservicios sólo pagan si usted entiende las transacciones

Los microservicios no son un punto de partida, son un resultado.

**La complejidad debe ser ganada.** Los sistemas distribuidos no son una insignia, son un instrumento de deuda.

La ecuación de compensación es simple:

```
Microservices Value = (Team Autonomy + Independent Scaling + Failure Isolation)
                     - (Latency Tax + SerDe Tax + Operational Overhead + Coordination Cost)
```

Para la mayoría de los equipos - especialmente los productos de primera etapa o de un solo equipo - el lado derecho domina. Usted paga costos enormes por los beneficios que no necesita todavía.

Pero cuando tienes:

* 5+ equipos pisando los dedos de los pies del otro
* Colas de despliegue medidas en días
* Subsistemas con necesidades de escala muy diferentes
* Requisitos reglamentarios para el aislamiento de datos

...entonces el lado izquierdo comienza a ganar. El impuesto se justifica.

### Marco de decisiones

Haga estas preguntas en orden:

1. **¿Puede un equipo seguir siendo dueño de este extremo a extremo?**  
   → Sí: permanecer monolito. No: considerar la división.

2. **¿Los equipos están bloqueados por los ciclos de liberación del otro?**  
   → Sí: los microservicios podrían ayudar. No: la coordinación está funcionando.

3. **¿Tenemos el músculo operativo para ejecutar los sistemas distribuidos?**  
   → No: construirlo primero. Sí: proceder con cuidado.

4. **¿Podemos rastrear, depurar y desplegar múltiples servicios sin heroísmo?**  
   → No: no estás listo. Sí: podrías estarlo.

5. **¿Hemos probado los límites en código primero?**  
   → No: fija tu estructura monolítica. Sí: la extracción es más segura.

Si no puedes pasar la pregunta 3 con confianza, no estás listo para microservicios - y eso está bien. **La arquitectura aburrida es una ventaja competitiva.**

La mayoría de los productos exitosos fueron construidos como monolitos primero: GitHub, Shopify, Stack Overflow, Basecamp. Ellos evolucionaron en sistemas distribuidos sólo cuando el dolor organizacional lo exigía.

Su trabajo no es construir la arquitectura más impresionante. Es entregar valor mientras minimiza la complejidad accidental.

Y ningún cliente ha pagado nunca más porque su sistema usó Kafka.

A veces eso significa microservicios. Por lo general, significa un monolito bien estructurado y la disciplina para mantenerlo de esa manera.