InterviewHack.ai
Start free
Blog/Preguntas de entrevista de Microservicios — 35 respuestas de arquitecto

Preguntas de entrevista de Microservicios — 35 respuestas de arquitecto

16 de septiembre de 2026

microservicessystem-design

35 preguntas de microservicios para roles backend y arquitecto: descomposición de servicios, saga pattern, event sourcing, circuit breaker, observabilidad. Respuestas de nivel senior.

Preguntas de entrevista de Microservicios — 35 respuestas de arquitecto

Si estás preparando una entrevista para un rol de backend senior, staff engineer o arquitecto de software, casi seguro que te van a preguntar sobre microservicios. No las preguntas superficiales de "¿qué es un microservicio?", sino las que van al hueso: ¿cómo manejás consistencia eventual entre servicios? ¿Cuándo usás Kafka en lugar de REST? ¿Qué hacés cuando un servicio se cae a la mitad de una saga?

Este artículo te da 35 preguntas con respuestas de nivel arquitecto, con ejemplos de código concretos y los errores que más ven los entrevistadores.


Parte 1: Monolito vs Microservicios — Trade-offs reales

1. ¿Cuándo tiene sentido migrar de monolito a microservicios?

La respuesta que buscan: Que no respondas "siempre" ni "nunca", sino que razonés los trade-offs.

La migración tiene sentido cuando el monolito tiene problemas de escala organizacional (5+ equipos tocando el mismo repo, deploys que bloquean a todos) o técnicos concretos: un módulo necesita escalar 100x independiente del resto, o una parte del sistema necesita un stack diferente (por ejemplo, procesamiento de imágenes en Python mientras el resto corre en Java).

Lo que no justifica migrar:

  • "Todos lo hacen" — la moda no es arquitectura
  • Performance genérica — primero perfilá el monolito
  • Equipos chicos (menos de 3 devs) — el overhead operacional te mata

Un buen heurístico: si tu monolito tarda menos de 10 minutos en hacer deploy y tu equipo es menor a 20 personas, probablemente no necesitás microservicios todavía.

Error común en entrevistas: Decir que los microservicios son siempre más performantes. En realidad, una llamada in-process es órdenes de magnitud más rápida que una llamada de red. Los microservicios sacrifican latencia local por escalabilidad independiente.


2. ¿Cuáles son los costos reales de adoptar microservicios?

Los costos que los libros no te cuentan:

Latencia de red: Cada llamada entre servicios suma. Si tu flujo de pago pasa por 6 servicios en cadena, estás acumulando decenas de milisegundos que antes no existían.

Consistencia de datos: Ya no tenés transacciones ACID entre servicios. Tenés consistencia eventual, que es mucho más difícil de razonar y depurar.

Complejidad operacional: Necesitás service mesh, distributed tracing, centralized logging, health checks, múltiples pipelines de CI/CD. Un monolito bien estructurado tiene cero de eso.

Testing de integración: Los tests de contrato entre servicios son difíciles. Consumer-driven contracts (Pact) ayudan pero agregan friction.

Debugging distribuido: "El usuario no puede pagar" en un monolito es un stack trace. En microservicios es una investigación que requiere correlacionar logs de 4 servicios.


3. ¿Qué es un monolito modular y cuándo lo preferís sobre microservicios?

Un monolito modular es un monolito donde los módulos tienen límites claros: no se llaman directamente entre capas internas, exponen interfaces bien definidas, y podrían convertirse en servicios separados si algún día fuera necesario.

monolith/
├── modules/
│   ├── orders/
│   │   ├── OrderService.ts       ← interfaz pública del módulo
│   │   ├── OrderRepository.ts
│   │   └── internal/             ← nadie de afuera importa de acá
│   ├── payments/
│   │   ├── PaymentService.ts
│   │   └── internal/
│   └── inventory/
│       ├── InventoryService.ts
│       └── internal/

Lo preferís cuando:

  • El equipo es chico y la velocidad importa más que la escala independiente
  • Necesitás transacciones ACID entre entidades del negocio
  • Tu dominio no tiene límites claros todavía (una startup en etapa temprana)

El truco es que si lo diseñás bien, migrar a microservicios después es mucho más fácil porque los límites ya existen.


Parte 2: Descomposición por dominio (DDD)

4. ¿Cómo descomponés un sistema en servicios usando DDD?

La clave es el Bounded Context. Un bounded context es una frontera explícita dentro de la cual un modelo de dominio es consistente. En un e-commerce:

  • Catálogo: Un "Producto" tiene nombre, descripción, categorías, imágenes
  • Inventario: Un "Producto" tiene SKU, stock, ubicación en warehouse
  • Pedidos: Un "Producto" tiene precio en el momento de la compra, cantidad, descuentos aplicados

Son tres modelos distintos del mismo concepto real. Forzarlos en una sola entidad crea el God Object que hace al sistema inmantenible.

Cómo identificar los bounded contexts:

  1. 1Event storming: mapear los domain events del negocio en un pizarrón
  2. 2Identificar aggregates (clusters de objetos que cambian juntos)
  3. 3Ver qué equipos de negocio "poseen" cada parte del dominio

Error común: Descomponer por capa técnica (un servicio para "todos los repos", otro para "toda la lógica de negocio"). Eso no es DDD, es distribuir una arquitectura en capas — obtenés todo el overhead de microservicios sin los beneficios.


5. ¿Qué es un Aggregate en DDD y cómo afecta el diseño de tus servicios?

Un Aggregate es un cluster de entidades y value objects que se tratan como una unidad para propósitos de consistencia. El Aggregate Root es la única entrada al aggregate.

typescript
// Order es el aggregate root
class Order {
  private id: OrderId;
  private items: OrderItem[];  // solo se accede via Order
  private status: OrderStatus;

  addItem(product: ProductId, quantity: number, price: Money): void {
    if (this.status !== OrderStatus.DRAFT) {
      throw new Error('No se pueden agregar items a un pedido confirmado');
    }
    // invariante: máximo 50 items por pedido
    if (this.items.length >= 50) {
      throw new Error('Límite de items alcanzado');
    }
    this.items.push(new OrderItem(product, quantity, price));
  }

  confirm(): OrderConfirmed {
    if (this.items.length === 0) {
      throw new Error('No se puede confirmar un pedido vacío');
    }
    this.status = OrderStatus.CONFIRMED;
    return new OrderConfirmed(this.id, this.items);
  }
}

Implicación para microservicios: Un aggregate nunca debería partirse entre dos servicios. Si para guardar consistencia necesitás una transacción ACID que toca dos aggregates, probablemente esos aggregates pertenecen al mismo servicio.


6. ¿Cómo manejás referencias entre servicios? ¿Guardás copias de datos o hacés llamadas en tiempo real?

La respuesta canónica: usás identificadores y tolerás datos eventuales.

En vez de que el servicio de Orders llame al servicio de Users cada vez que necesita el nombre del cliente, Orders guarda userId y opcionalmente una copia desnormalizada de los datos que necesita (nombre, email al momento del pedido).

Hay tres estrategias:

1. Solo IDs (purista): Orders solo guarda userId. Para mostrar el nombre, la UI llama a ambos servicios. Simple, pero la UI se complica.

2. Copia en el evento: Cuando se crea un pedido, el evento OrderCreated incluye el userId y el customerName en ese momento. Orders lo guarda.

3. Cache local via eventos: Orders escucha eventos de Users (UserNameUpdated) y actualiza su copia local. Más complejo pero permite queries locales.

La estrategia correcta depende de qué tan crítica es la freshness del dato. Para el nombre del cliente en un pedido histórico, la copia en el evento es perfecta. Para "¿está bloqueada esta cuenta?", necesitás la llamada en tiempo real.


Parte 3: Comunicación entre servicios

7. ¿Cuándo usás REST y cuándo usás gRPC para comunicación síncrona?

| Criterio | REST/HTTP | gRPC |

|---|---|---|

| Clientes heterogéneos (browser, mobile) | Sí | No fácil (necesita grpc-web) |

| Latencia crítica entre servicios internos | No | Sí |

| Contrato estricto entre equipos | OpenAPI | Protobuf |

| Streaming bidireccional | No nativo | Sí |

| Simplicidad operacional | Alta | Media |

Cuándo elegir gRPC:

  • Comunicación interna entre servicios donde controlás ambos lados
  • Necesitás serialización binaria para reducir payload (protobuf es ~3-5x más compacto que JSON)
  • Tenés streaming de datos (telemetría, feeds en tiempo real)

Cuándo elegir REST:

  • APIs públicas o consumidas por browsers
  • Equipos que no quieren gestionar schemas de protobuf
  • Necesitás que sea debuggeable con curl
proto
// Ejemplo de contrato gRPC
syntax = "proto3";

service OrderService {
  rpc GetOrder (GetOrderRequest) returns (Order);
  rpc StreamOrderUpdates (GetOrderRequest) returns (stream OrderUpdate);
}

message GetOrderRequest {
  string order_id = 1;
}

message Order {
  string id = 1;
  string customer_id = 2;
  repeated OrderItem items = 3;
  OrderStatus status = 4;
}

8. ¿Cuándo preferís comunicación asíncrona (Kafka/RabbitMQ) sobre síncrona?

Comunicación asíncrona es mejor cuando:

  1. 1El emisor no necesita la respuesta inmediata. Un servicio de notificaciones no necesita esperar a que el email se envíe.
  1. 2Necesitás desacoplar picos de carga. Si el servicio de procesamiento de pagos puede manejar 100 req/s pero llegás a 1000 req/s en el Black Friday, una cola absorbe el pico.
  1. 3Necesitás que múltiples consumidores reaccionen al mismo evento. Cuando se confirma un pedido, Inventario lo descuenta, Facturación emite la factura, y Notificaciones manda el email. Con eventos, cada uno escucha independientemente.
  1. 4Tolerás latencia adicional a cambio de resiliencia. Si el servicio de email se cae, el mensaje queda en la cola y se procesa cuando vuelva.

El error clásico: Usar mensajería asíncrona para todo, incluyendo queries que necesitan respuesta en el mismo request HTTP del usuario. Si el usuario hace clic en "Ver mis pedidos", no podés decirle "te aviso en unos segundos".

Kafka vs RabbitMQ:

  • Kafka: eventos de alto volumen, replay de mensajes, múltiples consumer groups, retention a largo plazo
  • RabbitMQ: colas de trabajo simples, routing flexible, cuando necesitás ACK por mensaje individual

9. ¿Qué es el patrón Outbox y por qué es crítico con mensajería asíncrona?

El problema: cuando guardás datos en la DB y publicás un evento al broker, tenés dos operaciones que pueden fallar independientemente.

typescript
// MAL — race condition entre DB y broker
async function confirmOrder(orderId: string) {
  await db.orders.update({ id: orderId, status: 'CONFIRMED' });
  // Si esto falla, el pedido está confirmado en DB pero nadie lo sabe
  await kafka.produce('order-confirmed', { orderId });
}

El patrón Outbox resuelve esto: guardás el evento en la misma transacción de base de datos, en una tabla "outbox". Un proceso separado (el relay) lee esa tabla y publica al broker.

sql
-- La tabla outbox
CREATE TABLE outbox_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  aggregate_type VARCHAR(100) NOT NULL,
  aggregate_id VARCHAR(100) NOT NULL,
  event_type VARCHAR(100) NOT NULL,
  payload JSONB NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  published_at TIMESTAMPTZ
);
typescript
// BIEN — todo en una transacción
async function confirmOrder(orderId: string) {
  await db.transaction(async (tx) => {
    await tx.orders.update({ id: orderId, status: 'CONFIRMED' });
    await tx.outboxEvents.insert({
      aggregate_type: 'Order',
      aggregate_id: orderId,
      event_type: 'OrderConfirmed',
      payload: { orderId, confirmedAt: new Date() }
    });
  });
  // El relay se encarga de publicar al broker de forma eventual
}

Debezium es la herramienta más usada para implementar el relay usando Change Data Capture (CDC) sobre el transaction log de Postgres.


Parte 4: Patrón Saga

10. ¿Qué es el patrón Saga y cuándo lo usás?

Una Saga es una secuencia de transacciones locales donde cada transacción publica un evento que dispara la siguiente. Si algún paso falla, se ejecutan transacciones compensatorias para deshacer lo que ya se hizo.

Cuándo lo necesitás: Cuando tenés un flujo de negocio que toca múltiples servicios y necesitás consistencia eventual (no podés usar una transacción distribuida XA porque no querés ese acoplamiento).

Ejemplo — checkout de e-commerce:

1. Servicio de Pedidos: Crear pedido (status: PENDING)
   → Publica: OrderCreated

2. Servicio de Inventario: Reservar stock
   → Publica: StockReserved | StockUnavailable

3. Servicio de Pagos: Procesar pago
   → Publica: PaymentProcessed | PaymentFailed

4. Servicio de Pedidos: Confirmar pedido (status: CONFIRMED)
   → Publica: OrderConfirmed

Si el pago falla:

  • Pagos publica: PaymentFailed
  • Inventario escucha y libera el stock (transacción compensatoria)
  • Pedidos escucha y cancela el pedido

11. ¿Cuál es la diferencia entre Saga coreografiada y orquestada?

Coreografía (Choreography): Cada servicio sabe qué hacer cuando recibe un evento. No hay coordinador central.

Orders → [OrderCreated] → Inventory escucha y reserva
Inventory → [StockReserved] → Payments escucha y cobra
Payments → [PaymentFailed] → Inventory escucha y libera stock

Ventajas: Bajo acoplamiento, cada servicio es autónomo.

Desventajas: El flujo completo está distribuido entre servicios. Difícil de ver el estado global. Fácil que nadie sepa exactamente qué eventos disparan qué.

Orquestación (Orchestration): Un Saga Orchestrator coordina el flujo. Les dice a los servicios qué hacer y maneja las compensaciones.

typescript
class CheckoutSaga {
  async execute(orderId: string) {
    try {
      await this.inventoryService.reserveStock(orderId);
      await this.paymentService.processPayment(orderId);
      await this.orderService.confirmOrder(orderId);
    } catch (error) {
      // rollback coordinado
      await this.compensate(orderId, error);
    }
  }

  async compensate(orderId: string, failedAt: Error) {
    if (failedAt instanceof PaymentError) {
      await this.inventoryService.releaseStock(orderId);
    }
    await this.orderService.cancelOrder(orderId);
  }
}

Ventajas: El flujo es visible en un solo lugar, más fácil de debuggear.

Desventajas: El orquestador puede convertirse en un service de Dios si no se tiene cuidado.

Cuándo elegir cada uno: Para flujos simples (2-3 pasos), coreografía. Para flujos complejos con muchas rutas de error, orquestación. En la práctica, muchos equipos usan orquestación con Temporal o AWS Step Functions.


12. ¿Cómo manejás la idempotencia en un sistema de sagas?

Los mensajes pueden llegar duplicados (el broker garantiza at-least-once delivery). Si el servicio de pagos procesa el mismo mensaje dos veces, cobrás dos veces.

Solución: idempotency keys

typescript
// El productor genera un ID único por intento
const idempotencyKey = `payment-${orderId}-${attemptNumber}`;

// El consumidor verifica si ya procesó ese key
async function processPayment(event: PaymentRequested) {
  const alreadyProcessed = await db.processedEvents.findOne({
    key: event.idempotencyKey
  });

  if (alreadyProcessed) {
    return alreadyProcessed.result; // devolvé el resultado anterior
  }

  const result = await chargeCard(event);

  // Guardá en la misma transacción
  await db.transaction(async (tx) => {
    await tx.payments.insert(result);
    await tx.processedEvents.insert({
      key: event.idempotencyKey,
      result,
      processedAt: new Date()
    });
  });

  return result;
}

Parte 5: Event Sourcing y CQRS

13. ¿Qué es Event Sourcing y cuándo tiene sentido usarlo?

En un sistema tradicional, guardás el estado actual. En Event Sourcing, guardás la secuencia de eventos que llevaron a ese estado. El estado actual se deriva aplicando todos los eventos en orden.

typescript
// Estado tradicional: una fila que se actualiza
// { id: '123', balance: 1500, status: 'active' }

// Event Sourcing: la secuencia de hechos
const events = [
  { type: 'AccountOpened',  data: { initialDeposit: 1000 }, at: '...' },
  { type: 'MoneyDeposited', data: { amount: 500 },          at: '...' },
  { type: 'MoneyWithdrawn', data: { amount: 200 },          at: '...' },
  // Estado derivado: 1000 + 500 - 200 = 1300... espera, el balance era 1500
  // Ah, hay otro evento que no mostré
]

// Para reconstruir el estado actual:
function replayEvents(events: DomainEvent[]): AccountState {
  return events.reduce((state, event) => {
    switch (event.type) {
      case 'AccountOpened':
        return { ...state, balance: event.data.initialDeposit };
      case 'MoneyDeposited':
        return { ...state, balance: state.balance + event.data.amount };
      case 'MoneyWithdrawn':
        return { ...state, balance: state.balance - event.data.amount };
      default:
        return state;
    }
  }, {} as AccountState);
}

Cuándo tiene sentido:

  • Auditoría es un requisito legal (finanzas, salud)
  • Necesitás poder "viajar en el tiempo" al estado en cualquier punto
  • Los eventos de dominio son ciudadanos de primera clase del negocio
  • Necesitás proyecciones múltiples del mismo dato

Cuándo NO lo uses:

  • En un CRUD simple donde no hay historia relevante
  • Si tu equipo no tiene experiencia — la curva de aprendizaje es real
  • Cuando el volumen de eventos crece sin snapshots — reconstruir el estado se vuelve costoso

14. ¿Qué es CQRS y cómo se combina con Event Sourcing?

CQRS (Command Query Responsibility Segregation) separa las operaciones de escritura (commands) de las de lectura (queries) en modelos distintos.

El problema que resuelve: El mismo modelo optimizado para writes (validaciones, invariantes de dominio) es pésimo para reads complejos que necesitan joins y aggregations.

typescript
// WRITE SIDE: el modelo de dominio rico
class OrderAggregate {
  apply(command: ConfirmOrder): OrderConfirmed {
    if (this.items.length === 0) throw new Error('Pedido vacío');
    if (this.status !== 'DRAFT') throw new Error('Ya confirmado');
    return new OrderConfirmed(this.id, this.items, new Date());
  }
}

// READ SIDE: proyecciones optimizadas para queries
// Una proyección para el dashboard de ventas
interface SalesDashboardProjection {
  totalRevenue: number;
  ordersByStatus: Record<string, number>;
  topProducts: { productId: string; units: number }[];
}

// Un event handler actualiza la proyección cuando llegan eventos
class SalesDashboardProjector {
  on(event: OrderConfirmed): void {
    // actualizar tabla de read model
    db.salesDashboard.upsert({
      date: event.confirmedAt.toDateString(),
      revenue: event.total
    });
  }
}

Combinado con Event Sourcing: Los events que se persisten en el event store disparan los projectors que actualizan los read models. El read model puede ser una tabla SQL optimizada, Elasticsearch, Redis — lo que sea más rápido para ese tipo de query.

El trade-off: Consistencia eventual. El read model puede estar un segundo atrás del write model. En la mayoría de los contextos esto está bien. Si no lo está, probablemente no deberías usar CQRS.


15. ¿Cómo manejás snapshots en Event Sourcing para evitar replay de miles de eventos?

Si una cuenta bancaria tiene 10 años de transacciones, reconstruir el estado requeriría procesar miles de eventos en cada request. Los snapshots resuelven esto.

typescript
async function loadAggregate(aggregateId: string): Promise<Account> {
  // 1. Buscar el snapshot más reciente
  const snapshot = await eventStore.getLatestSnapshot(aggregateId);

  let state: AccountState;
  let fromVersion: number;

  if (snapshot) {
    state = snapshot.state;
    fromVersion = snapshot.version + 1;
  } else {
    state = Account.initialState();
    fromVersion = 0;
  }

  // 2. Cargar solo los eventos DESPUÉS del snapshot
  const events = await eventStore.getEvents(aggregateId, fromVersion);

  // 3. Aplicar solo los eventos nuevos
  return events.reduce(applyEvent, state);
}

async function saveAggregate(account: Account, newEvents: DomainEvent[]) {
  await eventStore.appendEvents(account.id, newEvents);

  // Guardar snapshot cada 100 eventos
  if (account.version % 100 === 0) {
    await eventStore.saveSnapshot({
      aggregateId: account.id,
      version: account.version,
      state: account.currentState()
    });
  }
}

Parte 6: Resiliencia — Circuit Breaker y patrones relacionados

16. ¿Qué es el Circuit Breaker y cómo funciona?

El Circuit Breaker previene que fallas en cascada destruyan todo el sistema. Si el servicio B se está cayendo, no tiene sentido que el servicio A siga golpeándolo con requests — solo agrega latencia y consume threads.

Los tres estados:

CLOSED → (fallas superan umbral) → OPEN → (timeout) → HALF-OPEN
  ↑                                                         |
  └──────── (llamada exitosa) ──────────────────────────────┘
  • CLOSED: Todo normal, las llamadas pasan
  • OPEN: El circuito está abierto, las llamadas fallan rápido sin ir al servicio
  • HALF-OPEN: Deja pasar algunas llamadas para probar si el servicio se recuperó
typescript
class CircuitBreaker {
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
  private failureCount = 0;
  private lastFailureTime?: Date;

  constructor(
    private readonly failureThreshold = 5,
    private readonly timeout = 60000 // 60 segundos
  ) {}

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime!.getTime() > this.timeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new CircuitOpenError('Circuit breaker is open');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }

  private onFailure() {
    this.failureCount++;
    this.lastFailureTime = new Date();
    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
    }
  }
}

// Uso
const paymentBreaker = new CircuitBreaker(5, 30000);

async function processPayment(order: Order) {
  try {
    return await paymentBreaker.call(() =>
      paymentService.charge(order)
    );
  } catch (error) {
    if (error instanceof CircuitOpenError) {
      // Fallback: poner en cola para reintentar después
      await queue.enqueue('pending-payments', order);
      return { status: 'QUEUED', message: 'Pago en proceso' };
    }
    throw error;
  }
}

En producción usás Resilience4j (Java), Polly (.NET), o la configuración del service mesh (Istio/Linkerd) en lugar de implementarlo vos.


17. ¿Qué es el patrón Bulkhead?

El Bulkhead (mamparo) aísla los recursos para que una falla en un área no drene los recursos de otra.

El ejemplo clásico: si tenés un pool de 20 threads para llamar servicios externos y el Servicio A empieza a responder lento, esos 20 threads se llenan esperando a A y ninguna otra request puede procesarse.

typescript
// Con bulkhead: pools separados por servicio
const paymentPool = new ThreadPool({ maxThreads: 5 });
const inventoryPool = new ThreadPool({ maxThreads: 5 });
const notificationPool = new ThreadPool({ maxThreads: 3 });

// Ahora si Payments se cae, solo afecta su pool de 5 threads
// Inventory y Notifications siguen funcionando

En la práctica esto se implementa a nivel de configuración de Hystrix/Resilience4j o del service mesh.


18. ¿Qué estrategias de retry usás y cómo evitás sobrecargar un servicio caído?

Retry con backoff exponencial + jitter:

typescript
async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxAttempts = 3
): Promise<T> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts - 1) throw error;

      // Backoff exponencial: 1s, 2s, 4s...
      const baseDelay = Math.pow(2, attempt) * 1000;
      // Jitter: ±20% aleatorio para evitar thundering herd
      const jitter = baseDelay * 0.2 * (Math.random() * 2 - 1);
      const delay = baseDelay + jitter;

      await sleep(delay);
    }
  }
  throw new Error('Max attempts reached');
}

El thundering herd problem: Si 1000 clientes hacen retry exactamente al mismo tiempo (porque todos tienen el mismo backoff sin jitter), el servicio recibe 1000 requests simultáneos justo cuando intenta recuperarse. El jitter los dispersa.

Qué no hacer: Retry en loops infinitos sin backoff. Retry en errores 4xx (son errores del cliente, no del servidor — reintentar no cambia nada).


Parte 7: API Gateway y Service Discovery

19. ¿Cuál es el rol del API Gateway y qué no debería hacer?

Lo que hace:

  • Routing de requests a los servicios correctos
  • Autenticación y autorización (verificar JWT antes de que llegue a los servicios)
  • Rate limiting y throttling
  • SSL termination
  • Agregación de responses para reducir round-trips (BFF pattern)

Lo que NO debería hacer:

  • Lógica de negocio — si el gateway sabe cuándo aplicar un descuento, ya es un servicio de negocio
  • Transformaciones complejas de datos — eso es responsabilidad del servicio o del BFF
  • Ser el único punto de entrada para comunicación interna entre servicios
yaml
# Ejemplo con Kong/KrakenD
routes:
  - path: /api/v1/orders
    service: orders-service
    plugins:
      - name: jwt
        config:
          secret: ${JWT_SECRET}
      - name: rate-limiting
        config:
          minute: 100
          hour: 1000

  - path: /api/v1/payments
    service: payments-service
    plugins:
      - name: jwt
      - name: rate-limiting
        config:
          minute: 20  # más restrictivo para pagos

20. ¿Qué es el patrón Backend for Frontend (BFF)?

Un BFF es un API Gateway especializado para un cliente específico (mobile app, web app, third-party API). En lugar de tener una API genérica que sirve a todos, cada cliente tiene su propia API optimizada para sus necesidades.

El problema que resuelve: La web necesita datos diferentes a los que necesita mobile. Mobile puede necesitar que varios endpoints se combinen en uno para reducir round-trips. Forzar a un solo API a servir a todos crea endpoints feos con 40 campos donde cada cliente usa 10 distintos.

[Mobile App] → [Mobile BFF] → [Orders Service]
                            → [User Service]
                            → [Inventory Service]

[Web App] → [Web BFF] → [Orders Service]
                      → [User Service]

[Partner API] → [Partner BFF] → [Orders Service] (acceso limitado)

21. ¿Cómo funciona el Service Discovery y cuándo lo necesitás?

Cuando tenés 20 instancias de un servicio que escalan dinámicamente, los clientes no pueden tener las IPs hardcodeadas. Service Discovery resuelve esto.

Client-side discovery (Eureka/Consul):

El cliente consulta el registry para obtener las instancias disponibles y decide a cuál llamar.

Server-side discovery (AWS ALB, Kubernetes):

El cliente llama a un load balancer que sabe dónde están las instancias.

En Kubernetes, esto viene out-of-the-box:

yaml
# El servicio actúa como service discovery
apiVersion: v1
kind: Service
metadata:
  name: payment-service
spec:
  selector:
    app: payment-service  # encuentra pods con este label
  ports:
    - port: 8080
# Ahora cualquier pod llama a "payment-service:8080"
# Kubernetes resuelve a la IP correcta

En sistemas no-Kubernetes, Consul con health checks es la opción más común.


Parte 8: Trazabilidad distribuida

22. ¿Cómo implementás observabilidad en microservicios?

Los tres pilares:

Logs: Correlacionados con un trace ID para poder seguir una request a través de servicios.

typescript
// Cada request genera un traceId y lo propaga
app.use((req, res, next) => {
  req.traceId = req.headers['x-trace-id'] || generateUUID();
  res.setHeader('x-trace-id', req.traceId);

  logger.info('Request received', {
    traceId: req.traceId,
    method: req.method,
    path: req.path,
    service: 'orders-service'
  });

  next();
});

// Al llamar otro servicio, propagar el traceId
async function callInventoryService(orderId: string, traceId: string) {
  return fetch(`${INVENTORY_URL}/reserve`, {
    headers: { 'x-trace-id': traceId }
  });
}

Métricas: Contadores, gauges y histogramas con Prometheus.

typescript
import { Counter, Histogram } from 'prom-client';

const requestCounter = new Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'path', 'status']
});

const responseTime = new Histogram({
  name: 'http_response_time_seconds',
  help: 'Response time in seconds',
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2]
});

Trazas distribuidas: OpenTelemetry + Jaeger/Zipkin para visualizar el camino completo de una request.


23. ¿Qué métricas son las más importantes para monitorear en microservicios?

Los Golden Signals de Google SRE:

  1. 1Latency: No solo el promedio — el p95 y p99. Un servicio que responde en 100ms p50 pero 5 segundos p99 es un servicio con problemas.
  1. 2Traffic: Requests por segundo. Útil para detectar drops de tráfico (algo roto en el flujo) o spikes inesperados.
  1. 3Errors: Tasa de errores 5xx. Alert si sube más del 1%.
  1. 4Saturation: CPU, memoria, threads en uso. Un servicio cerca del límite va a degradarse antes de caerse.

Métricas específicas de microservicios:

  • Tamaño de la cola de mensajes (si usás Kafka/RabbitMQ)
  • Lag del consumer group en Kafka
  • Tiempo de reconexión del circuit breaker
  • Rate de mensajes en el dead letter queue

24. ¿Cómo manejás el distributed tracing con OpenTelemetry?

typescript
import { NodeSDK } from '@opentelemetry/sdk-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { trace, context, propagation } from '@opentelemetry/api';

// Inicialización
const sdk = new NodeSDK({
  traceExporter: new JaegerExporter({ endpoint: 'http://jaeger:14268/api/traces' }),
  serviceName: 'orders-service',
});
sdk.start();

// Instrumentación de una operación
async function createOrder(customerId: string, items: Item[]) {
  const tracer = trace.getTracer('orders-service');

  return tracer.startActiveSpan('createOrder', async (span) => {
    span.setAttribute('customer.id', customerId);
    span.setAttribute('items.count', items.length);

    try {
      const order = await db.orders.create({ customerId, items });

      // Al llamar otro servicio, el context se propaga automáticamente
      // si usás fetch con el middleware de OTel
      await inventoryService.reserve(order.id, items);

      span.setStatus({ code: SpanStatusCode.OK });
      return order;
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw error;
    } finally {
      span.end();
    }
  });
}

Con esto, en Jaeger podés ver el waterfall completo: orders-service → inventory-service → warehouse-service, con los tiempos exactos de cada hop.


Parte 9: Base de datos por servicio

25. ¿Por qué cada microservicio debería tener su propia base de datos?

Compartir una base de datos entre servicios destruye el aislamiento que justifica tener microservicios en primer lugar:

  • Acoplamiento de schema: Si el servicio A modifica una tabla que el servicio B usa, B puede romperse con un deploy de A
  • Acoplamiento de escala: Si B necesita un índice que le conviene a él pero que degrada las writes de A, hay conflicto
  • Sin encapsulamiento: Cualquier servicio puede leer datos del dominio de otro, saltándose las reglas de negocio

La regla: Un servicio es dueño de sus datos. Otros servicios acceden a esos datos a través de la API del servicio, nunca directo a la base.

¿Qué pasa con los reportes que necesitan datos de múltiples servicios?

Usás un data warehouse o un read model dedicado que agrega datos via eventos. No une tablas de distintos servicios.


26. ¿Cómo manejás queries que necesitan datos de múltiples servicios?

Opción 1: API Composition — La capa de presentación (BFF o GraphQL gateway) hace múltiples llamadas y compone la respuesta.

typescript
// GraphQL resolver que compone datos de varios servicios
const resolvers = {
  Order: {
    customer: async (order: Order) =>
      await userService.getUser(order.customerId),

    items: async (order: Order) =>
      await inventoryService.getProductDetails(order.itemIds),

    shippingEstimate: async (order: Order) =>
      await shippingService.estimate(order)
  }
};

Opción 2: CQRS con proyecciones cross-service — Un servicio de lectura escucha eventos de múltiples servicios y construye un read model desnormalizado.

typescript
// El servicio de reportes escucha eventos de Orders y Users
class OrderReportProjector {
  on(event: OrderConfirmed) {
    this.db.orderReports.upsert({
      orderId: event.orderId,
      customerId: event.customerId,
      total: event.total,
      // estos datos vinieron de un evento previo de Users
      customerName: this.customerCache.get(event.customerId)
    });
  }

  on(event: UserNameUpdated) {
    // actualizar las filas existentes
    this.db.orderReports.updateWhere(
      { customerId: event.userId },
      { customerName: event.newName }
    );
  }
}

27. ¿Qué estrategia usás para migrations de schema en un sistema con múltiples servicios?

Principio fundamental: las migraciones deben ser backwards-compatible porque en el momento del deploy, el servicio nuevo y el viejo corren en paralelo (rolling deployment).

El patrón expand-contract (o parallel change):

Fase 1 — Expand: Agregar la nueva columna como nullable
ALTER TABLE orders ADD COLUMN customer_email VARCHAR(255);

Fase 2 — Migrate: Mientras el servicio nuevo llena la columna nueva,
el servicio viejo sigue usando la columna vieja.
UPDATE orders SET customer_email = (SELECT email FROM users WHERE id = orders.customer_id);

Fase 3 — Contract: Una vez que todos los deployments usan la nueva columna,
eliminar la vieja.
ALTER TABLE orders DROP COLUMN old_customer_reference;

Herramientas: Flyway o Liquibase en Java; Alembic en Python; Prisma Migrate en Node.

Error común: Hacer DROP de una columna en la misma migration que agrega la nueva. Si el rollback del deploy ocurre, la columna vieja ya no existe y el servicio anterior falla.


Parte 10: Migración con Strangler Fig

28. ¿Cómo migrás un monolito a microservicios sin parar el sistema?

El patrón Strangler Fig (higuera estranguladora) — el árbol original sigue en pie mientras la nueva planta crece alrededor hasta reemplazarlo completamente.

Los pasos:

  1. 1Poner un proxy/API Gateway frente al monolito. Todo el tráfico pasa por él, pero por ahora todo va al monolito.
[Clientes] → [API Gateway] → [Monolito]
  1. 2Extraer el primer bounded context como microservicio. Empezar por algo que sea relativamente independiente (notificaciones, generación de reportes, autenticación).
  1. 3Redirigir el tráfico del feature migrado al nuevo servicio.
[Clientes] → [API Gateway] → [Monolito] (80% del tráfico)
                           → [Auth Service] (todas las /auth/*)
  1. 4Migrar gradualmente. Feature por feature, redirigir tráfico al nuevo servicio. El monolito "se estrangula" de a poco.

Qué migrar primero: Los módulos con mayor carga (para ganar escala independiente) o los módulos con equipos que se bloquean más (para ganar velocidad de delivery). Nunca el módulo más acoplado — ese es el último.


29. ¿Cómo manejás la sincronización de datos durante una migración Strangler Fig?

Durante la migración, los datos pueden estar en el monolito y en el nuevo servicio al mismo tiempo. Necesitás un período de "doble escritura":

typescript
// Fase de transición: escribir en ambos lados
async function updateUser(userId: string, data: UpdateUserDto) {
  // Escribir en el monolito (fuente de verdad por ahora)
  await legacyDb.users.update(userId, data);

  // Escribir en el nuevo servicio (en modo shadow)
  try {
    await userService.update(userId, data);
  } catch (error) {
    // Loggear pero no fallar — el monolito es la source of truth
    logger.error('Shadow write failed', { userId, error });
  }
}

Una vez que el nuevo servicio es la fuente de verdad, invertís: escribís al nuevo servicio y en modo shadow al monolito hasta que el monolito ya no necesite esos datos.


Parte 11: Preguntas de diseño del sistema

30. Diseñá el sistema de notificaciones para un e-commerce con microservicios.

Requisitos: Enviar email, push notification y SMS cuando se confirma un pedido, se actualiza el estado de un envío, o falla un pago.

Diseño:

[Order Service] → publica OrderConfirmed event
[Shipping Service] → publica ShipmentUpdated event
[Payment Service] → publica PaymentFailed event

↓ todos van a Kafka topic: domain-events

[Notification Service] consume:
  ├── Email channel (SendGrid)
  ├── Push channel (Firebase)
  └── SMS channel (Twilio)

Detalles importantes:

typescript
// El Notification Service tiene su propia tabla de preferencias
interface NotificationPreference {
  userId: string;
  channels: {
    email: boolean;
    push: boolean;
    sms: boolean;
  };
  events: Record<EventType, boolean>;
}

// Template engine por event type
class NotificationService {
  async handle(event: DomainEvent) {
    const prefs = await this.preferences.get(event.userId);
    const template = await this.templates.get(event.type);

    const notifications = [];

    if (prefs.channels.email) {
      notifications.push(this.emailSender.send({
        to: event.userEmail,
        subject: template.email.subject,
        body: template.email.render(event.data)
      }));
    }

    // Enviar en paralelo
    await Promise.allSettled(notifications);
    // allSettled: si el email falla, igual intentamos el push
  }
}

31. ¿Cómo diseñás para alta disponibilidad en microservicios?

Los principios:

Eliminar single points of failure: Toda instancia de servicio debe tener al menos 2 réplicas. El API Gateway, el message broker, la base de datos — todo en HA.

Health checks bien configurados: No solo "¿el proceso está vivo?" sino "¿puede manejar tráfico?". Kubernetes distingue liveness (reiniciar si está colgado) de readiness (sacar del load balancer si no está listo).

yaml
livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /health/ready  # verifica DB connection, etc
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

Graceful shutdown: El servicio deja de aceptar nuevas requests, termina las que está procesando, y recién ahí se apaga.

typescript
process.on('SIGTERM', async () => {
  server.close(); // no aceptar nuevas conexiones
  await waitForPendingRequests(); // esperar las actuales
  await db.close(); // cerrar conexiones
  process.exit(0);
});

32. ¿Cómo manejás el versionado de APIs en microservicios?

Estrategias:

URL versioning: /api/v1/orders, /api/v2/orders — simple y visible.

Header versioning: Accept: application/vnd.myapp.v2+json — más limpio pero menos discernible.

Backward compatibility como regla: Antes de llegar a v2, intentar que v1 siga funcionando. Agregar campos es backwards-compatible, remover o renombrar no lo es.

typescript
// Versión 1: devuelve customerName como string
// { orderId: '123', customerName: 'Juan López' }

// Versión 2: devuelve customer como objeto (más info)
// { orderId: '123', customer: { id: '456', name: 'Juan López', email: '...' } }

// Opción backwards-compatible: devolver ambos durante transición
// { orderId: '123', customerName: 'Juan López', customer: { ... } }

Consumer-driven contracts con Pact: El consumidor define qué campos necesita. El proveedor se asegura de que esos campos siempre existan. Si el proveedor quiere borrar un campo, los tests del consumidor fallan y te avisa antes del deploy.


33. ¿Qué es un service mesh y cuándo lo necesitás?

Un service mesh (Istio, Linkerd) es una capa de infraestructura que maneja la comunicación entre servicios. Se implementa como sidecar proxies (un proxy por pod) que interceptan todo el tráfico.

Lo que te da:

  • mTLS automático entre servicios (zero-trust networking)
  • Circuit breaker y retry a nivel de infraestructura (sin código en el servicio)
  • Distributed tracing automático
  • Traffic splitting para canary deployments

Cuándo lo necesitás:

  • Más de 20-30 microservicios
  • Requisitos de seguridad estrictos (mTLS entre servicios)
  • Querés observabilidad sin instrumentar manualmente cada servicio

El costo: Overhead operacional significativo. Istio tiene una curva de aprendizaje empinada. Para equipos chicos o sistemas simples, puede ser sobredimensionado.


34. ¿Cómo testéas microservicios? ¿Cuál es la diferencia entre integration y contract tests?

La pirámide de tests para microservicios:

           /  E2E  \           ← Pocos, lentos, caros
          /----------\
         / Contract   \        ← Testeás el contrato entre servicios
        /--------------\
       / Integration    \      ← Testeás el servicio con sus dependencias reales (DB, etc)
      /------------------\
     /    Unit Tests       \   ← Muchos, rápidos, baratos
    /----------------------\

Unit tests: Testean la lógica de dominio sin dependencias externas. Muy rápidos.

Integration tests: Testean el servicio con su base de datos real, pero mockeando otros servicios.

Contract tests (Pact): El consumidor define un contrato (qué request hace, qué response espera). El proveedor corre ese contrato contra su API real para verificar que lo cumple. Detectan breaking changes antes del deploy.

typescript
// Consumer test (Pact)
describe('Order Service calls User Service', () => {
  it('should get user by id', async () => {
    await provider.addInteraction({
      state: 'User 123 exists',
      uponReceiving: 'a request for user 123',
      withRequest: { method: 'GET', path: '/users/123' },
      willRespondWith: {
        status: 200,
        body: {
          id: '123',
          name: like('Juan López'), // acepta cualquier string
          email: email() // acepta cualquier email válido
        }
      }
    });

    const user = await userClient.getUser('123');
    expect(user.id).toBe('123');
  });
});

35. ¿Cómo diseñás para multi-tenancy en microservicios?

Tres modelos:

Pool model: Todos los tenants comparten la misma infraestructura. Más económico, más complejo de aislar.

typescript
// Tenant ID en cada request
app.use((req, res, next) => {
  req.tenantId = req.headers['x-tenant-id'];
  next();
});

// Todas las queries filtran por tenantId
async function getOrders(tenantId: string, userId: string) {
  return db.orders.findMany({
    where: { tenantId, userId } // siempre filtrar ambos
  });
}

Silo model: Cada tenant tiene su propia infraestructura. Máximo aislamiento, costo lineal con el número de tenants.

Hybrid: Tenants grandes tienen su propio silo, el resto comparte el pool.

El error común: Olvidar el tenantId en algún query y exponer datos entre tenants. En sistemas con datos sensibles, esto es un incidente de seguridad grave. Patron recomendado: middleware que inyecta el tenantId automáticamente a todos los repositorios.


Preparación para la entrevista: lo que más evalúan los entrevistadores

Lo que separa a un senior de un mid:

  1. 1Trade-offs explícitos. No decir "Event Sourcing es lo mejor" sino "Event Sourcing me da X, me cuesta Y, tiene sentido cuando Z".
  1. 2Experiencia con el dolor. Contar que "implementamos saga coreografiada y el debugging fue un infierno porque el flujo estaba distribuido en 4 servicios" muestra experiencia real.
  1. 3No saltar a microservicios. Si el problema que te dan se puede resolver con un monolito bien estructurado, decirlo es una señal de madurez.
  1. 4Operaciones. Los entrevistadores de nivel senior preguntan sobre observabilidad, deployment, migración de datos. Si solo hablás de código y no de cómo operás el sistema, es una señal de menos experiencia.
  1. 5Cuándo evitar patrones. Saber cuándo NO usar event sourcing, cuándo NO usar microservicios, cuándo una base de datos compartida puede ser una opción razonable.

Recursos para seguir practicando

Para prepararte mejor, practicá explicar estos patrones en voz alta — la diferencia entre entender un concepto y poder articularlo bajo presión en una entrevista es enorme. Grabate, cronometrate, y prestá atención a dónde tartamudeás o perdés el hilo: esos son los temas donde necesitás más práctica.

Los temas que más aparecen en entrevistas de arquitectura según nivel:

  • Senior Backend: Sagas, outbox pattern, circuit breaker, API design
  • Staff Engineer: Event sourcing/CQRS, migración de sistemas legacy, multi-tenancy
  • Principal/Architect: Trade-offs organizacionales, Conway's Law, decisiones de make vs buy

FAQ

¿Qué diferencia hay entre microservicios y arquitectura orientada a servicios (SOA)?+

SOA surgió como una forma de conectar sistemas empresariales heterogéneos usando un Enterprise Service Bus (ESB) central. Los microservicios son más pequeños, se despliegan independientemente, evitan el ESB (prefieren comunicación directa o colas ligeras), y cada servicio posee su propia base de datos. En SOA los servicios solían compartir una base de datos común y el bus central se convertía en un cuello de botella y punto de falla.

¿Cuántos microservicios es 'demasiados'?+

No hay un número mágico, pero una señal de alerta es cuando tenés más servicios que desarrolladores que los entienden. Otro heurístico: si para implementar una feature siempre tenés que tocar 5+ servicios en sincronía, probablemente tus límites de servicio están mal diseñados. El tamaño correcto es el que permite a un equipo pequeño (2-5 personas) poseer, deployar y operar el servicio de forma autónoma.

¿Cómo manejo las transacciones que abarcan múltiples microservicios?+

No usás transacciones distribuidas (2PC/XA) en microservicios modernos porque crean acoplamiento fuerte y problemas de disponibilidad. En su lugar usás el patrón Saga: una secuencia de transacciones locales con compensaciones. Si algo falla en el paso 3, ejecutás transacciones compensatorias para deshacer los pasos 1 y 2. La consistencia es eventual, no inmediata, pero el sistema es mucho más resiliente.

¿Qué es la consistencia eventual y cómo la explico en una entrevista?+

Consistencia eventual significa que, si no llegan nuevas actualizaciones a un dato, eventualmente todas las réplicas convergerán al mismo valor. En microservicios, cuando el servicio A actualiza un dato y publica un evento, el servicio B puede tardar milisegundos o segundos en procesar ese evento y actualizar su copia. Durante ese intervalo, B tiene un dato 'viejo'. La clave en una entrevista es mostrar que entendés cuándo esto es aceptable (nombre del cliente en un historial de pedidos) y cuándo no (saldo disponible en una cuenta bancaria).

¿Para qué sirve Kafka vs RabbitMQ en microservicios?+

Kafka es un log distribuido de eventos: los mensajes persisten por días/semanas, múltiples consumer groups pueden leer el mismo topic de forma independiente, y podés hacer replay de eventos históricos. Es ideal para event-driven architectures, event sourcing y alta throughput. RabbitMQ es un message broker tradicional: los mensajes se borran cuando son consumidos, tiene routing flexible con exchanges y bindings, y es más simple para colas de trabajo punto a punto. Elegís Kafka cuando necesitás replay, múltiples consumidores independientes o volúmenes altos. RabbitMQ para flujos de trabajo simples o cuando el mensaje solo necesita ser procesado una vez por un consumidor.

¿Cómo me preparo para una entrevista de system design de microservicios?+

Practicá explicar los trade-offs en voz alta, no solo memorizarlos. Los entrevistadores no buscan que recites definiciones, buscan que razonés: '¿cuándo uso X vs Y y por qué?'. Practicá con escenarios concretos: diseñá el sistema de pagos de un e-commerce, o la autenticación de una plataforma SaaS. Prestá especial atención a los casos de falla: ¿qué pasa si se cae el servicio de inventario a mitad de un checkout? ¿Qué pasa si un mensaje se procesa dos veces? Esas preguntas son las que separan al senior del mid.

Related articles

Preguntas de entrevista de diseño de sistemas distribuidos (35+)

Complete system design interview article in Spanish rioplatense with 40 questions, detailed answers, real code

Preguntas de entrevista de GraphQL — 35 con código y respuestas

35 preguntas de GraphQL para entrevistas backend y full-stack: queries, mutations, subscriptions, resolvers, DataLoader, N+1 problem, schema design. Con código.

Preguntas de entrevista de Redis — 35 con código y respuestas reales

35 preguntas de Redis para entrevistas backend y system design: estructuras de datos, pub/sub, persistencia, clustering, patrones de caché. Con ejemplos de código.

System Design: cómo diseñar un sistema de pagos en una entrevista

Guía paso a paso para diseñar un sistema de pagos en una entrevista de system design: idempotencia, consistencia, colas de transacciones, reconciliación, seguridad PCI DSS.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY