Skip to main content

Introducción a la Arquitectura Distribuida

3. Introducción a la Arquitectura Distribuida

3.1 ¿Qué significa que un sistema sea distribuido?

Definición Fundamental

Un sistema distribuido es un conjunto de componentes de software que se ejecutan en múltiples computadoras, coordinándose para lograr un objetivo común, y que se presentan ante los usuarios como un sistema único y coherente.

A diferencia de un monolito donde todo se ejecuta en un único proceso, en un sistema distribuido:

  1. Componentes independientes: Cada parte del sistema se ejecuta en su propio proceso, frecuentemente en diferentes máquinas o contenedores.
  2. Comunicación por red: Los componentes se comunican mediante protocolos de red (HTTP, gRPC, mensajería) en lugar de llamadas a funciones en memoria.
  3. Fallos independientes: Un componente puede fallar sin necesariamente afectar a los demás.
  4. Escalado independiente: Cada componente puede escalarse horizontalmente según sus necesidades específicas.

La Diferencia Crítica: Aplicación vs Sistema

Aplicación Monolítica: Un único programa que contiene toda la lógica de negocio, interfaz de usuario y acceso a datos. Se ejecuta como una unidad indivisible.

┌─────────────────────────────────────────────┐
│ PROCESO ÚNICO DEL MONOLITO │
├───────────┬───────────┬─────────────────────┤
│ Web │ Lógica │ Acceso a │
│ Server │ Negocio │ Datos │
│ (Express)│ (Services)│ (Models) │
├───────────┴───────────┴─────────────────────┤
│ Memoria Compartida │
└─────────────────────────────────────────────┘

┌─────▼─────┐
│ Base de │
│ Datos │
│ Única │
└───────────┘

Sistema Distribuido: Múltiples aplicaciones (servicios) que colaboran, cada una con responsabilidades específicas.

Imagina el sistema distribuido como varias aplicaciones separadas, cada una viviendo por su cuenta, y un punto central que las conecta.

Explicación paso a paso del gráfico

El cliente es quien hace la petición, por ejemplo un navegador o una app móvil. El cliente no habla directamente con todos los servicios, solo conoce una dirección.

El API Gateway es la puerta de entrada. Su trabajo es recibir la petición del cliente y decidir a qué servicio o servicios debe llamar. A veces solo llama a uno. Otras veces llama a varios y combina las respuestas.

Cada servicio es una aplicación independiente:

  • Se ejecuta en su propio proceso
  • Escucha en su propio puerto
  • Puede arrancarse o apagarse sin afectar a los demás
  • Tiene una responsabilidad muy concreta

Cada servicio tiene su propia base de datos. No se comparten tablas ni conexiones. Esto es clave para entender la diferencia con un monolito.

La idea clave que debes quedarte

En un monolito, todo está dentro de una sola caja.

En un sistema distribuido:

  • Hay muchas cajas
  • Cada caja hace una cosa
  • Las cajas se comunican por red
  • Si una caja falla, las demás pueden seguir funcionando

El gráfico no intenta mostrar código ni tecnología concreta, sino una idea fundamental: el sistema es un conjunto de aplicaciones que colaboran, no una única aplicación grande.

Características de los Sistemas Distribuidos

  1. Transparencia: Los usuarios no deberían percibir que están interactuando con un sistema distribuido. Debe comportarse como un sistema único.
  2. Concurrencia: Múltiples componentes pueden ejecutarse simultáneamente en diferentes nodos.
  3. Falta de reloj global: No existe un tiempo universal preciso compartido por todos los componentes.
  4. Fallos independientes: Un componente puede fallar mientras otros continúan funcionando.
  5. Escalabilidad horizontal: La capacidad del sistema puede aumentarse añadiendo más nodos.

Analogía del Mundo Real: Una Cadena de Restaurantes

Monolito: Un pequeño restaurante familiar donde:

  • Un mismo chef prepara todas las comidas
  • El mesero también maneja la caja registradora
  • El dueño limpia, cocina y atiende
  • Todo ocurre en un mismo espacio físico

Sistema Distribuido: Una cadena de restaurantes como McDonald's donde:

  • Cocina centralizada: Prepara ingredientes para múltiples locales
  • Locales distribuidos: Cada restaurante sirve en su ubicación
  • Sistema de pedidos central: Coordina entregas a domicilio
  • App móvil: Los clientes ordenan desde cualquier lugar
  • Cada componente puede fallar sin colapsar todo el sistema
  • Cada restaurante puede escalar independientemente según su ubicación

3.2 Diferencia entre Aplicación y Sistema

Aplicación: Unidad Funcional Independiente

Una aplicación es un programa de software diseñado para realizar un conjunto específico de funciones para el usuario final. En contexto distribuido, cada servicio ES una aplicación independiente.

Características de una aplicación en sistemas distribuidos:

  • Tiene un propósito específico y bien delimitado.
  • Se puede desarrollar, desplegar y escalar independientemente.
  • Tiene su propio ciclo de vida.
  • Puede usar tecnologías diferentes a otras aplicaciones del sistema

Sistema: Colección Coordinada de Aplicaciones

Un sistema es el conjunto completo de aplicaciones que trabajan juntas para proporcionar una funcionalidad completa al usuario.

Características de un sistema distribuido:

  • Está compuesto por múltiples aplicaciones (servicios).
  • Las aplicaciones se comunican mediante APIs bien definidas.
  • El sistema proporciona una funcionalidad que ninguna aplicación individual podría proporcionar sola
  • Tiene propiedades emergentes que surgen de la interacción entre aplicaciones

Ejemplo Práctico: Plataforma de E-Commerce

SISTEMA DE E-COMMERCE (Sistema Distribuido)
├── 🛒 Aplicación: Servicio de Catálogo
├── 💳 Aplicación: Servicio de Pagos
├── 📦 Aplicación: Servicio de Inventario
├── 👤 Aplicación: Servicio de Usuarios
├── 🚚 Aplicación: Servicio de Envíos
└── 📧 Aplicación: Servicio de Notificaciones

Cada aplicación:
- Tiene su propia base de datos
- Se despliega independientemente
- Usa su stack tecnológico óptimo
- Falla independientemente

3.3 Analogías del Mundo Real para Sistemas Distribuidos

1. Sistema Postal Internacional

Componentes distribuidos:

  • Oficinas postales locales (servicios independientes).
  • Centros de distribución regional (message brokers).
  • Transporte aéreo/terrestre (protocolos de comunicación).
  • Sistemas de tracking (observabilidad distribuida).

Características distribuidas:

  • Fallas parciales: Un camión puede romperse sin detener todo el sistema.
  • Escalabilidad: Se pueden añadir más camiones o aviones según la demanda.
  • Latencia: Los paquetes tardan tiempo en llegar (network latency).
  • Consistencia eventual: El tracking se actualiza cuando llega a cada punto.

2. Sistema Nervioso Humano

Componentes distribuidos:

  • Neuronas individuales (microservicios).
  • Sinapsis (comunicación entre servicios).
  • Sistemas especializados (visual, motor, digestivo).
  • Médula espinal (message bus).

Características distribuidas:

  • Procesamiento paralelo: Múltiples estímulos se procesan simultáneamente
  • Tolerancia a fallos: Daño en un área no paraliza todo el sistema
  • Comunicación asíncrona: Señales químicas y eléctricas con diferentes latencias
  • Especialización: Diferentes partes especializadas en funciones específicas

3. Orquesta Sinfónica

Componentes distribuidos:

  • Músicos individuales (servicios)
  • Secciones (grupos de servicios relacionados)
  • Director (orquestador/API Gateway)
  • Partituras (contratos/APIs)

Características distribuidas:

  • Coordinación sin centralización: Cada músico sigue la partitura
  • Comunicación implícita: A través de la música compartida
  • Fallas degradantes: Si un violinista se equivoca, la sinfonía continúa
  • Escalabilidad: Se pueden añadir más músicos a secciones específicas

3.4 Casos Reales donde se usa Arquitectura Distribuida

1. Netflix: Streaming Global

Problema: Servir contenido a 200+ millones de usuarios simultáneamente en todo el mundo.

Solución distribuida:

  • Servicio de Recomendaciones: Machine learning para recomendaciones personalizadas.
  • Servicio de Streaming: Entrega de video adaptativo.
  • Servicio de Perfiles: Gestión de múltiples perfiles por cuenta.
  • CDN Distribuida: Contenido cacheado cerca de los usuarios.
  • Servicio de Búsqueda: Indexación y búsqueda de contenido.

Beneficios:

  • Escala global mediante regiones geográficas.
  • Fallos en una región no afectan a otras.
  • Actualizaciones independientes por servicio.

2. Uber: Movilidad en Tiempo Real

Problema: Conectar conductores y pasajeros en tiempo real en ciudades de todo el mundo.

Solución distribuida:

  • Servicio de Geoubicación: Tracking en tiempo real.
  • Servicio de Matching: Algoritmos de emparejamiento.
  • Servicio de Pagos: Procesamiento distribuido de transacciones.
  • Servicio de Notificaciones: Push notifications a millones de dispositivos.
  • Servicio de Precios: Cálculo dinámico de tarifas.

Beneficios:

  • Baja latencia para operaciones críticas.
  • Escalado por ciudad/región.
  • Resiliencia ante fallos de componentes.

3. Amazon: E-Commerce a Escala Planetaria

Problema: Manejar millones de transacciones diarias, inventario global y logística compleja.

Solución distribuida:

  • Servicio de Catálogo: Billones de productos con búsqueda.
  • Servicio de Carrito: Estado de carrito distribuido.
  • Servicio de Recomendaciones: "Los clientes que compraron X...".
  • Servicio de Inventario: Stock en tiempo real en múltiples almacenes.
  • Servicio de Órdenes: Procesamiento de pedidos distribuido.

Beneficios:

  • Disponibilidad 99.99% durante Black Friday.
  • Escalado independiente por servicio.
  • Desarrollo paralelo por miles de ingenieros.

4. Twitter (ahora X): Timeline en Tiempo Real

Problema: Procesar 500+ millones de tweets diarios y servir timelines personalizados.

Solución distribuida:

  • Servicio de Tweets: Almacenamiento y recuperación de tweets
  • Servicio de Timeline: Construcción de feed personalizado
  • Servicio de Seguidores: Grafos sociales distribuidos
  • Servicio de Búsqueda: Indexación en tiempo real
  • Servicio de Notificaciones: Alertas de interacciones

Beneficios:

  • Baja latencia para timeline delivery
  • Escalado para eventos globales (Super Bowl, elecciones)
  • Tolerancia a picos de tráfico masivos

Ejemplo Práctico: Sistema de Blog Distribuido

Vamos a implementar un sistema de blog distribuido simple que demuestre los conceptos fundamentales. Este sistema tendrá 3 servicios independientes que colaboran.

Estructura del Sistema

sistema-blog-distribuido/
├── servicio-articulos/
│ ├── package.json
│ ├── server.js
│ └── database.js
├── servicio-comentarios/
│ ├── package.json
│ ├── server.js
│ └── database.js
├── servicio-usuarios/
│ ├── package.json
│ ├── server.js
│ └── database.js
└── api-gateway/
├── package.json
└── server.js

Servicio 1: Artículos

servicio-articulos/package.json:

{
"name": "servicio-articulos",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"sqlite3": "^5.1.6",
"sqlite": "^5.1.1",
"axios": "^1.6.0"
}
}

servicio-articulos/server.js:

// servicio-articulos/server.js
// Programa principal del microservicio de artículos.
// Responsabilidad: arrancar HTTP, preparar esquema local y exponer endpoints del dominio "artículos".

import express from "express";
import { getDB } from "./database.js";

const app = express();
const PORT = 3001;

app.use(express.json());

// Conexión a BD del servicio (local a este proceso).
let db;

async function initDB() {
db = await getDB();

// Esquema local del servicio.
// Importante: ninguna otra app debe tocar esta tabla directamente.
await db.exec(`
CREATE TABLE IF NOT EXISTS articulos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
contenido TEXT NOT NULL,
autor_id INTEGER NOT NULL,
fecha_publicacion DATETIME DEFAULT CURRENT_TIMESTAMP,
estado TEXT DEFAULT 'publicado'
);

CREATE INDEX IF NOT EXISTS idx_articulos_autor ON articulos(autor_id);
CREATE INDEX IF NOT EXISTS idx_articulos_fecha ON articulos(fecha_publicacion);
`);
}

// Health check: confirma que el proceso está vivo.
app.get("/health", (req, res) => {
res.json({
service: "articulos",
status: "healthy",
timestamp: new Date().toISOString(),
port: PORT
});
});

// Lista de artículos publicados.
app.get("/articulos", async (req, res) => {
try {
const articulos = await db.all(`
SELECT id, titulo, autor_id, fecha_publicacion
FROM articulos
WHERE estado = 'publicado'
ORDER BY fecha_publicacion DESC
`);

res.json(articulos);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// Detalle de un artículo publicado.
app.get("/articulos/:id", async (req, res) => {
try {
const articulo = await db.get(
"SELECT * FROM articulos WHERE id = ? AND estado = 'publicado'",
[req.params.id]
);

if (!articulo) return res.status(404).json({ error: "Artículo no encontrado" });

res.json(articulo);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// Crear un artículo.
app.post("/articulos", async (req, res) => {
try {
const { titulo, contenido, autor_id } = req.body;

// Validación mínima (la capa de dominio/servicio podría endurecer reglas).
if (!titulo || !contenido || !autor_id) {
return res.status(400).json({
error: "Faltan campos requeridos: titulo, contenido, autor_id"
});
}

const result = await db.run(
"INSERT INTO articulos (titulo, contenido, autor_id) VALUES (?, ?, ?)",
[titulo, contenido, autor_id]
);

res.status(201).json({
id: result.lastID,
titulo,
contenido,
autor_id,
fecha_publicacion: new Date().toISOString(),
estado: "publicado"
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});

async function startService() {
// No arrancar HTTP hasta tener BD lista.
await initDB();

app.listen(PORT, () => {
console.log(`Servicio de Artículos: http://localhost:${PORT}`);
console.log(`Health: http://localhost:${PORT}/health`);
console.log(`Artículos: http://localhost:${PORT}/articulos`);
});
}

startService().catch(console.error);

servicio-articulos/server.js es el punto de arranque del microservicio de artículos. Al ejecutarlo, se levanta un proceso Node.js independiente que escucha en su propio puerto y expone una API para gestionar artículos.

Dentro del servicio, este archivo se encarga de inicializar la base de datos local, definir los endpoints del dominio de artículos y ofrecer un endpoint de salud (/health) para comprobar que el proceso está vivo.

La idea que demuestra es simple: en una arquitectura distribuida, cada dominio (artículos, usuarios, comentarios) se convierte en una aplicación autónoma con su propio ciclo de vida, sus propios datos y comunicación con el resto únicamente por HTTP, normalmente a través del API Gateway.

servicio-articulos/database.js:

// servicio-articulos/database.js
// Capa de datos del microservicio de artículos.
// Responsabilidad: abrir la base de datos local del servicio y devolver un handle con promesas.
//
// Regla de arquitectura (microservicios):
// - Cada servicio es dueño de su almacenamiento.
// - El resto del sistema no accede a este archivo .db directamente.

import sqlite3 from "sqlite3";
import { open } from "sqlite";

/**
* getDB()
* Abre (o crea si no existe) la base de datos del servicio.
* Devuelve un objeto db compatible con async/await:
* - db.get() -> una fila
* - db.all() -> varias filas
* - db.run() -> INSERT/UPDATE/DELETE
* - db.exec() -> ejecutar SQL sin resultados
*/
export async function getDB() {
const db = await open({
filename: "./articulos.db",
driver: sqlite3.Database
});

// Ajustes mínimos recomendables en SQLite para un servicio local:
// - foreign_keys: coherencia interna (aunque no uses FKs aquí, no molesta).
// - WAL: mejora concurrencia en lecturas/escrituras durante desarrollo.
await db.exec(`
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
`);

return db;
}

servicio-articulos/database.js es el módulo encargado de abrir y proporcionar acceso a la base de datos del servicio de artículos. Su responsabilidad se limita a conectar el servicio con su almacenamiento local.

No arranca servidores ni define lógica HTTP. Únicamente devuelve una conexión SQLite lista para ejecutar consultas, manteniendo los datos de artículos completamente aislados del resto del sistema.

Este archivo ilustra una diferencia clave frente al monolito: en una arquitectura distribuida, cada servicio es dueño de su base de datos. Los cambios en el esquema o en la forma de almacenar artículos no afectan a usuarios ni comentarios, reforzando la autonomía técnica de cada servicio.

Servicio 2: Comentarios

servicio-comentarios/package.json:

{
"name": "servicio-comentarios",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"sqlite3": "^5.1.6",
"sqlite": "^5.1.1"
}
}

servicio-comentarios/server.js:

// servicio-comentarios/server.js
// Programa principal del microservicio de comentarios.
// Responsabilidad: arrancar HTTP, preparar esquema local y exponer endpoints para comentarios por artículo.

import express from "express";
import { getDB } from "./database.js";

const app = express();
const PORT = 3002;

app.use(express.json());

let db;

async function initDB() {
db = await getDB();

// Tabla local del servicio de comentarios.
// articulo_id y usuario_id son referencias lógicas (ids externos), no claves foráneas reales entre servicios.
await db.exec(`
CREATE TABLE IF NOT EXISTS comentarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
articulo_id INTEGER NOT NULL,
usuario_id INTEGER NOT NULL,
contenido TEXT NOT NULL,
fecha_creacion DATETIME DEFAULT CURRENT_TIMESTAMP,
estado TEXT DEFAULT 'activo'
);

CREATE INDEX IF NOT EXISTS idx_comentarios_articulo ON comentarios(articulo_id);
`);

// Datos de ejemplo (seed) para pruebas locales.
// Debe estar dentro de initDB() para ejecutarse tras abrir BD y crear tabla.
const row = await db.get("SELECT COUNT(*) AS count FROM comentarios");
if (row.count === 0) {
await db.exec(`
INSERT INTO comentarios (articulo_id, usuario_id, contenido) VALUES
(1, 1, 'Excelente artículo, muy informativo'),
(1, 2, 'Gracias por compartir esta información'),
(2, 1, 'Interesante perspectiva sobre el tema');
`);
}
}

app.get("/health", (req, res) => {
res.json({
service: "comentarios",
status: "healthy",
timestamp: new Date().toISOString(),
port: PORT
});
});

// Comentarios activos de un artículo.
app.get("/articulos/:articuloId/comentarios", async (req, res) => {
try {
const comentarios = await db.all(
`SELECT id, articulo_id, usuario_id, contenido, fecha_creacion
FROM comentarios
WHERE articulo_id = ? AND estado = 'activo'
ORDER BY fecha_creacion DESC`,
[req.params.articuloId]
);

res.json(comentarios);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// Crear comentario en un artículo.
app.post("/articulos/:articuloId/comentarios", async (req, res) => {
try {
const { usuario_id, contenido } = req.body;

if (!usuario_id || !contenido) {
return res.status(400).json({
error: "Faltan campos requeridos: usuario_id, contenido"
});
}

const result = await db.run(
"INSERT INTO comentarios (articulo_id, usuario_id, contenido) VALUES (?, ?, ?)",
[req.params.articuloId, usuario_id, contenido]
);

res.status(201).json({
id: result.lastID,
articulo_id: Number(req.params.articuloId),
usuario_id,
contenido,
fecha_creacion: new Date().toISOString(),
estado: "activo"
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});

async function startService() {
await initDB();

app.listen(PORT, () => {
console.log(`Servicio de Comentarios: http://localhost:${PORT}`);
console.log(`Health: http://localhost:${PORT}/health`);
console.log(`Endpoint: GET/POST /articulos/:id/comentarios`);
});
}

startService().catch(console.error);

servicio-comentarios/server.js es el punto de arranque del microservicio de comentarios. Al ejecutarse, levanta un proceso Node.js independiente que escucha en su propio puerto y gestiona exclusivamente los comentarios asociados a artículos.

Este archivo inicializa la base de datos local de comentarios, define los endpoints para consultar y crear comentarios y expone un endpoint de salud para monitorización. No conoce la lógica interna de artículos ni usuarios; solo trabaja con identificadores externos.

El valor arquitectónico del ejemplo es mostrar que los servicios no comparten datos ni bases de datos. Cada servicio es autónomo, puede fallar o evolucionar de forma independiente y se comunica con el resto únicamente a través de APIs HTTP.

servicio-comentarios/database.js

// servicio-comentarios/database.js
// Capa de datos del microservicio de comentarios.
// Responsabilidad: abrir comentarios.db y devolver un handle para consultas async/await.

import sqlite3 from "sqlite3";
import { open } from "sqlite";

export async function getDB() {
const db = await open({
filename: "./comentarios.db",
driver: sqlite3.Database
});

await db.exec(`
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
`);

return db;
}

Servicio 3: Usuarios

servicio-usuarios/package.json:

{
"name": "servicio-usuarios",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"sqlite3": "^5.1.6",
"sqlite": "^5.1.1"
}
}

servicio-usuarios/server.js:

// servicio-usuarios/server.js
// Programa principal del microservicio de usuarios.
// Responsabilidad: arrancar HTTP, preparar esquema local y exponer endpoints de consulta/búsqueda.

import express from "express";
import { getDB } from "./database.js";

const app = express();
const PORT = 3003;

app.use(express.json());

let db;

async function initDB() {
// Abrimos la BD mediante la capa dedicada (evita errores de driver en ESM).
db = await getDB();

await db.exec(`
CREATE TABLE IF NOT EXISTS usuarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
fecha_registro DATETIME DEFAULT CURRENT_TIMESTAMP,
activo BOOLEAN DEFAULT 1
);
`);

// Seed para pruebas locales.
const row = await db.get("SELECT COUNT(*) AS count FROM usuarios");
if (row.count === 0) {
await db.exec(`
INSERT INTO usuarios (nombre, email) VALUES
('Ana García', 'ana@ejemplo.com'),
('Carlos López', 'carlos@ejemplo.com'),
('María Rodríguez', 'maria@ejemplo.com');
`);
}
}

app.get("/health", (req, res) => {
res.json({
service: "usuarios",
status: "healthy",
timestamp: new Date().toISOString(),
port: PORT
});
});

// Listado de usuarios activos.
app.get("/usuarios", async (req, res) => {
try {
const usuarios = await db.all(
"SELECT id, nombre, email, fecha_registro FROM usuarios WHERE activo = 1"
);
res.json(usuarios);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// Usuario por id (activo).
app.get("/usuarios/:id", async (req, res) => {
try {
const usuario = await db.get(
"SELECT id, nombre, email, fecha_registro FROM usuarios WHERE id = ? AND activo = 1",
[req.params.id]
);

if (!usuario) return res.status(404).json({ error: "Usuario no encontrado" });

res.json(usuario);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// Búsqueda por nombre o email (LIKE).
app.get("/usuarios/buscar/:query", async (req, res) => {
try {
const term = `%${req.params.query}%`;

const usuarios = await db.all(
`SELECT id, nombre, email, fecha_registro
FROM usuarios
WHERE (nombre LIKE ? OR email LIKE ?) AND activo = 1`,
[term, term]
);

res.json(usuarios);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

async function startService() {
await initDB();

app.listen(PORT, () => {
console.log(`Servicio de Usuarios: http://localhost:${PORT}`);
console.log(`Health: http://localhost:${PORT}/health`);
console.log(`Usuarios: http://localhost:${PORT}/usuarios`);
});
}

startService().catch(console.error);

servicio-usuarios/server.js es el punto de arranque del microservicio de usuarios. Al ejecutarse, levanta un proceso Node.js independiente que escucha en su propio puerto y expone una API para consultar y buscar usuarios.

Este archivo inicializa la base de datos local de usuarios, define los endpoints del dominio de usuarios y ofrece un endpoint de salud para monitorización. No depende de artículos ni comentarios y no comparte datos con otros servicios.

Didácticamente, refuerza la idea central de la arquitectura distribuida: cada dominio se implementa como una aplicación autónoma, dueña de sus datos y de su ciclo de vida, que se comunica con el resto únicamente mediante APIs HTTP.

servicio-usuarios/database.js

import sqlite3 from "sqlite3";
import { open } from "sqlite";

export async function getDB() {
const db = await open({
filename: "./usuarios.db",
driver: sqlite3.Database
});

await db.exec(`
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
`);

return db;
}

API Gateway: Punto de Entrada Único

api-gateway/package.json:

{
"name": "api-gateway",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"axios": "^1.6.0",
"express-http-proxy": "^2.0.0"
}
}

api-gateway/server.js:

// api-gateway/server.js
// API Gateway del sistema distribuido.
// Responsabilidad:
// - Un único puerto público (3000) para el cliente.
// - Enrutar (proxy) peticiones a servicios internos.
// - Componer respuestas que requieren datos de varios servicios.
// - Agregar health checks del sistema.
//
// Nota: este gateway NO guarda datos propios. Coordina y enruta.

import express from "express";
import proxy from "express-http-proxy";
import axios from "axios";

const app = express();
const PORT = 3000;

app.use(express.json());

// Registro simple de servicios internos.
// En un entorno real esto podría venir de variables de entorno o service discovery.
const servicios = {
articulos: "http://localhost:3001",
comentarios: "http://localhost:3002",
usuarios: "http://localhost:3003"
};

// Helper para construir un health agregado sin tumbar el gateway si un servicio cae.
async function checkServiceHealth(nombre, baseUrl) {
try {
const { data } = await axios.get(`${baseUrl}/health`, { timeout: 1500 });
return { status: "healthy", data };
} catch (err) {
return { status: "unhealthy", error: err.message };
}
}

// Health agregado del sistema.
// No asume que todos los servicios están OK: si alguno falla, responde "degraded".
app.get("/health", async (req, res) => {
const healthChecks = {};

for (const [nombre, url] of Object.entries(servicios)) {
healthChecks[nombre] = await checkServiceHealth(nombre, url);
}

const todosSanos = Object.values(healthChecks).every(
(check) => check.status === "healthy"
);

res.json({
system: "blog-distribuido",
overall: todosSanos ? "healthy" : "degraded",
timestamp: new Date().toISOString(),
services: healthChecks,
architecture: "distributed"
});
});

// Endpoint compuesto: artículo + autor + comentarios.
// Este endpoint existe SOLO en el gateway (API composition).
app.get("/articulos-completos/:id", async (req, res) => {
try {
// 1) Artículo (si esto falla con 404, devolvemos 404 al cliente).
const articuloResponse = await axios.get(
`${servicios.articulos}/articulos/${req.params.id}`,
{ timeout: 2000 }
);
const articulo = articuloResponse.data;

// 2) Comentarios y autor en paralelo.
const comentariosPromise = axios.get(
`${servicios.comentarios}/articulos/${req.params.id}/comentarios`,
{ timeout: 2000 }
);

const autorPromise = axios.get(
`${servicios.usuarios}/usuarios/${articulo.autor_id}`,
{ timeout: 2000 }
);

// allSettled permite degradar parcialmente (sin romper toda la respuesta).
const [comentariosResponse, autorResponse] = await Promise.allSettled([
comentariosPromise,
autorPromise
]);

res.json({
...articulo,
autor:
autorResponse.status === "fulfilled" ? autorResponse.value.data : null,
comentarios:
comentariosResponse.status === "fulfilled"
? comentariosResponse.value.data
: [],
metadata: {
fetched_from: "multiple-services",
timestamp: new Date().toISOString()
}
});
} catch (error) {
if (error.response?.status === 404) {
return res.status(404).json({ error: "Artículo no encontrado" });
}

res.status(500).json({
error: "Error al obtener artículo completo",
details: error.message
});
}
});

// =======================
// PROXIES (pasarela simple)
// =======================
//
// Importante:
// - El servicio de artículos espera rutas /articulos...
// - Pero el gateway expone /api/articulos...
// Por eso reescribimos la ruta (recortamos el prefijo) antes de reenviar.

app.use(
"/api/articulos",
proxy(servicios.articulos, {
proxyReqPathResolver: (req) =>
req.originalUrl.replace(/^\/api\/articulos/, "")
})
);

app.use(
"/api/comentarios",
proxy(servicios.comentarios, {
proxyReqPathResolver: (req) =>
req.originalUrl.replace(/^\/api\/comentarios/, "")
})
);

app.use(
"/api/usuarios",
proxy(servicios.usuarios, {
proxyReqPathResolver: (req) =>
req.originalUrl.replace(/^\/api\/usuarios/, "")
})
);

// Ruta raíz informativa para pruebas rápidas.
app.get("/", (req, res) => {
res.json({
sistema: "Blog Distribuido",
descripcion: "Sistema distribuido con 3 servicios independientes",
arquitectura: "distribuida",
servicios: Object.keys(servicios).map((nombre) => ({
nombre,
url: servicios[nombre],
health: `${servicios[nombre]}/health`
})),
endpoints: {
health: "/health",
articulos_completos: "/articulos-completos/:id",
api_articulos: "/api/articulos",
api_comentarios: "/api/comentarios",
api_usuarios: "/api/usuarios"
}
});
});

app.listen(PORT, () => {
console.log("API GATEWAY - Sistema Distribuido de Blog");
console.log(`Puerto principal: http://localhost:${PORT}`);
console.log(`Health check del sistema: http://localhost:${PORT}/health`);
console.log(`Artículo completo: http://localhost:${PORT}/articulos-completos/1`);
console.log("SERVICIOS:");
console.log(`Artículos: ${servicios.articulos}`);
console.log(`Comentarios: ${servicios.comentarios}`);
console.log(`Usuarios: ${servicios.usuarios}`);
});

api-gateway/server.js es el punto de entrada único del sistema distribuido. Actúa como intermediario entre los clientes y los servicios internos, de modo que el cliente nunca se comunica directamente con artículos, comentarios o usuarios.

Este archivo escucha en un único puerto, enruta peticiones hacia los servicios correctos, compone respuestas que requieren datos de varios servicios y centraliza el health check del sistema completo. No almacena datos ni implementa lógica de negocio; su función es coordinar y redirigir.

Desde el punto de vista arquitectónico, el gateway oculta la complejidad interna, orquesta llamadas entre servicios y permite aislar fallos parciales sin romper todo el sistema. Didácticamente, muestra que un sistema distribuido necesita una capa de coordinación que convierta varios servicios independientes en una API única y coherente para el cliente.

Instrucciones de Ejecución

# 1. Crear estructura de directorios
mkdir sistema-blog-distribuido
cd sistema-blog-distribuido
mkdir servicio-articulos servicio-comentarios servicio-usuarios api-gateway

# 2. Para cada servicio, crear los archivos correspondientes:
# - servicio-articulos/package.json, server.js, database.js
# - servicio-comentarios/package.json, server.js
# - servicio-usuarios/package.json, server.js
# - api-gateway/package.json, server.js

# 3. En CADA directorio de servicio, instalar dependencias:
cd servicio-articulos
npm install

cd ../servicio-comentarios
npm install

cd ../servicio-usuarios
npm install

cd ../api-gateway
npm install

# 4. Abrir 4 terminales diferentes, una para cada servicio:

# Terminal 1 - Servicio de Artículos
cd servicio-articulos
npm start

# Terminal 2 - Servicio de Comentarios
cd servicio-comentarios
npm start

# Terminal 3 - Servicio de Usuarios
cd servicio-usuarios
npm start

# Terminal 4 - API Gateway
cd api-gateway
npm start

Pruebas útiles con REST Client (VSCode) vía API Gateway

Crea un archivo tests-gateway.http en la raíz del proyecto (o dentro de api-gateway/) y pega esto. Los endpoints expuestos por el gateway son /, /health, /articulos-completos/:id y los proxies bajo /api/*.

@base = http://localhost:3000
@json = application/json

### 1) Raíz del gateway: lista endpoints y servicios
GET {{base}}/
Accept: {{json}}

### 2) Health agregado: estado global + estado por servicio
GET {{base}}/health
Accept: {{json}}

### 3) Artículo compuesto: artículo + autor + comentarios
GET {{base}}/articulos-completos/1
Accept: {{json}}

### 4) Proxy a servicio de artículos: listar artículos publicados
# Requiere que el gateway reescriba el prefijo /api/articulos -> ''.
GET {{base}}/api/articulos/articulos
Accept: {{json}}

### 5) Proxy a servicio de artículos: detalle de artículo
GET {{base}}/api/articulos/articulos/1
Accept: {{json}}

### 6) Proxy a servicio de artículos: crear artículo
POST {{base}}/api/articulos/articulos
Content-Type: {{json}}
Accept: {{json}}

{
"titulo": "Artículo de prueba desde REST Client",
"contenido": "Contenido generado para probar POST a través del gateway.",
"autor_id": 1
}

### 7) Proxy a servicio de comentarios: listar comentarios de un artículo
GET {{base}}/api/comentarios/articulos/1/comentarios
Accept: {{json}}

### 8) Proxy a servicio de comentarios: crear comentario en un artículo
POST {{base}}/api/comentarios/articulos/1/comentarios
Content-Type: {{json}}
Accept: {{json}}

{
"usuario_id": 2,
"contenido": "Comentario creado a través del API Gateway."
}

### 9) Proxy a servicio de usuarios: listar usuarios activos
GET {{base}}/api/usuarios/usuarios
Accept: {{json}}

### 10) Proxy a servicio de usuarios: usuario por id
GET {{base}}/api/usuarios/usuarios/1
Accept: {{json}}

### 11) Proxy a servicio de usuarios: búsqueda por nombre o email
GET {{base}}/api/usuarios/usuarios/buscar/ana
Accept: {{json}}

### 12) Caso negativo: artículo inexistente (debe devolver 404 desde el gateway)
GET {{base}}/articulos-completos/999999
Accept: {{json}}

Nota operativa: si al probar los proxies /api/* ves 404 “raros”, el gateway está reenviando la ruta con el prefijo incluido. En ese caso, el proxy debe recortar /api/articulos, /api/comentarios, /api/usuarios antes de reenviar.

Demostración de Características Distribuidas

  1. Servicios Independientes:
    • Detén el servicio de comentarios (Ctrl+C en Terminal 2)
    • El sistema sigue funcionando, solo los comentarios no estarán disponibles
    • Los artículos y usuarios continúan funcionando
  2. Bases de Datos Separadas:
    • Cada servicio tiene su propio archivo .db
    • Puedes examinar articulos.db, comentarios.db, usuarios.db por separado
  3. Comunicación por Red:
    • El API Gateway hace llamadas HTTP a los otros servicios
    • Cada servicio expone una API REST independiente
  4. Fallas Parciales:
    • Si un servicio cae, los otros continúan funcionando
    • El health check del sistema muestra estado "degraded" pero no "down"