Arquitectura del Sistema de Microservicios para E-commerce
En esta lección, nos sumergiremos en la construcción del núcleo de un sistema de e-commerce moderno utilizando Go. La arquitectura se basará en dos microservicios independientes pero colaborativos: el Microservicio de Productos y el Microservicio de Pedidos. Cada uno poseerá su propia base de datos, lógica de negocio y API, comunicándose de manera eficiente y desacoplada. Este enfoque nos permite escalar horizontalmente cada componente según la demanda; por ejemplo, durante una campaña de ventas, el servicio de productos puede manejar un tráfico masivo de consultas sin afectar la estabilidad del procesamiento de pedidos.
La comunicación entre servicios será síncrona a través de HTTP/REST para operaciones simples y directas, manteniendo la simplicidad. Utilizaremos Gorilla/Mux como nuestro enrutador HTTP por su robustez, flexibilidad y desempeño en aplicaciones de alto rendimiento. Cada microservicio será un ejecutable Go independiente, gestionando su ciclo de vida, configuración y dependencias. La clave reside en definir contratos de API claros (esquemas de request/response) y mecanismos de manejo de errores consistentes para garantizar una experiencia fluida para el cliente frontend y otros servicios consumidores.
Para este proyecto integrador, estructuramos el código en capas claramente separadas: handlers (controladores HTTP), services (lógica de negocio), repositories (acceso a datos) y models (estructuras de datos). Esta separación de responsabilidades no solo facilita las pruebas unitarias e integrales, sino que también prepara el terreno para futuras evoluciones, como la sustitución de la capa de datos o la introducción de colas de mensajería para ciertas operaciones.
Concepto Clave: Desacoplamiento y Comunicación entre Servicios
El desacoplamiento es el principio arquitectónico fundamental detrás de los microservicios. Imagina una gran fábrica dividida en departamentos especializados: uno se encarga del inventario de piezas (Productos) y otro del ensamblaje y envío final (Pedidos). Cada departamento tiene sus propios almacenes (bases de datos), reglas internas (lógica de negocio) y un mostrador de atención (API). Para crear un producto final, el departamento de ensamblaje necesita solicitar piezas al de inventario. No van a revolver en el almacén ajeno; simplemente envían una solicitud formal por un tubo neumático (llamada HTTP) con el listado de piezas requeridas y esperan una respuesta.
En nuestro contexto, el Microservicio de Pedidos no accede directamente a la base de datos de Productos. En su lugar, realiza una llamada HTTP al endpoint expuesto por el Microservicio de Productos, por ejemplo, /api/products/validate-stock, para verificar la disponibilidad. Este contrato bien definido es el acoplamiento débil. Si el servicio de productos cambia su base de datos interna o su tecnología, mientras mantenga el mismo contrato de API, el servicio de pedidos no se verá afectado. La alternativa, un monolito con una base de datos compartida, sería como tener todos los departamentos en un solo caótico almacén, donde un cambio en la organización de las estanterías paralizaría a todos los trabajadores.
La comunicación síncrona HTTP, aunque introduce latencia, es adecuada para flujos donde se necesita una respuesta inmediata para continuar, como la validación de stock antes de confirmar un pedido. Para operaciones que no requieren respuesta inmediata (ej., enviar un correo de confirmación), se usarían patrones asíncronos, pero eso escapa al alcance de esta lección inicial. El manejo elegante de fallos en estas comunicaciones (timeouts, reintentos, circuit breakers) es lo que convierte un conjunto de servicios en un sistema resiliente.
Cómo Funciona en la Práctica: Flujo de Creación de un Pedido
Vamos a seguir, paso a paso, el flujo completo que ocurre cuando un cliente finaliza una compra en nuestra plataforma. Este es el corazón de la integración entre nuestros dos microservicios y ejemplifica la orquestación necesaria.
Paso 1: Solicitud del Cliente. La aplicación frontend o un API Gateway recibe la solicitud POST a /api/orders con un payload JSON que contiene el `user_id` y una lista de `items` (cada uno con `product_id` y `quantity`). Esta solicitud llega al handler correspondiente en el Microservicio de Pedidos.
Paso 2: Validación de Productos y Stock. El servicio de negocio de pedidos, antes de crear nada, debe validar que los productos existan y que haya stock suficiente. Para ello, construye una solicitud interna al Microservicio de Productos. Este es un punto crítico. El servicio de pedidos actuará como un cliente HTTP, utilizando un cliente configurado con timeout, para llamar a un endpoint como POST /api/products/batch-validation. Envía la lista de productos y cantidades solicitadas.
Paso 3: Respuesta del Servicio de Productos. El Microservicio de Productos recibe la solicitud, consulta su base de datos para cada producto, verifica el stock y calcula el precio unitario actual. Responde con un array detallado indicando, para cada ítem, si está disponible, el precio aplicado y el stock restante. Si algún producto no existe o no tiene stock, lo marca en la respuesta.
Paso 4: Procesamiento de la Respuesta y Creación. El servicio de pedidos recibe esta respuesta. Si todos los ítems están disponibles, procede a crear el pedido en su propia base de datos, con un estado inicial como `PENDIENTE_DE_PAGO`. Calcula el total sumando (precio * cantidad) para cada ítem. Si algún ítem no está disponible, aborta el proceso y retorna un error 422 (Unprocessable Entity) al cliente, detallando los problemas. Finalmente, responde al cliente con los detalles del pedido creado, incluyendo un ID único.
Tip Crítico: La validación de stock y la posterior reserva o descuento del mismo deben manejarse con cuidado en sistemas de alta concurrencia. El patrón mostrado aquí es de validación síncrona. Para evitar la "venta" del mismo producto a dos clientes simultáneamente (condición de carrera), el servicio de productos debe utilizar transacciones o mecanismos de bloqueo optimista (como versionado) en su base de datos al momento de verificar y actualizar el stock. Una alternativa más avanzada es el patrón de "Reserva de Stock" temporal.
Código en Acción: Implementación del Servicio de Pedidos con Llamada HTTP
A continuación, presentamos una implementación concreta y funcional de un segmento crucial del Microservicio de Pedidos, específicamente la capa de servicio que orquesta la creación de un pedido, incluyendo la comunicación HTTP con el servicio de productos.
Primero, definimos las estructuras de datos clave en un archivo `models.go`:
// models.go - Microservicio de Pedidos
package models
import "time"
type Order struct {
ID string `json:"id" bson:"_id,omitempty"`
UserID string `json:"user_id" bson:"user_id"`
Items []OrderItem `json:"items" bson:"items"`
Total float64 `json:"total" bson:"total"`
Status string `json:"status" bson:"status"` // PENDIENTE, PROCESADO, ENVIADO, CANCELADO
CreatedAt time.Time `json:"created_at" bson:"created_at"`
}
type OrderItem struct {
ProductID string `json:"product_id" bson:"product_id"`
ProductName string `json:"product_name,omitempty" bson:"product_name,omitempty"`
Quantity int `json:"quantity" bson:"quantity"`
UnitPrice float64 `json:"unit_price" bson:"unit_price"`
}
type CreateOrderRequest struct {
UserID string `json:"user_id" validate:"required"`
Items []CreateOrderItem `json:"items" validate:"required,min=1"`
}
type CreateOrderItem struct {
ProductID string `json:"product_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
Ahora, el núcleo de la lógica en el archivo `service/order_service.go`. Aquí se muestra la integración con el cliente HTTP para validar productos.
// service/order_service.go - Microservicio de Pedidos
package service
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
"tu-proyecto/models"
"tu-proyecto/repository"
)
// ProductServiceClient define la interfaz para la comunicación con el servicio de productos.
type ProductServiceClient interface {
ValidateProducts(items []models.CreateOrderItem) ([]models.ProductValidationResponse, error)
}
type HTTPProductServiceClient struct {
baseURL string
httpClient *http.Client
}
func NewHTTPProductServiceClient(baseURL string) *HTTPProductServiceClient {
return &HTTPProductServiceClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 5 * time.Second, // Timeout crítico para no bloquear el servicio
},
}
}
// ProductValidationRequest y Response son los contratos de la API de productos.
type ProductValidationRequest struct {
Items []models.CreateOrderItem `json:"items"`
}
type ProductValidationResponseItem struct {
ProductID string `json:"product_id"`
Available bool `json:"available"`
Name string `json:"name,omitempty"`
UnitPrice float64 `json:"unit_price"`
StockLeft int `json:"stock_left,omitempty"`
Message string `json:"message,omitempty"`
}
func (c *HTTPProductServiceClient) ValidateProducts(items []models.CreateOrderItem) ([]models.ProductValidationResponse, error) {
requestBody := ProductValidationRequest{Items: items}
jsonBody, err := json.Marshal(requestBody)
if err != nil {
return nil, fmt.Errorf("error al serializar la solicitud: %w", err)
}
req, err := http.NewRequest("POST", c.baseURL+"/api/products/batch-validation", bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("error al crear la solicitud HTTP: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
// Esto captura timeouts, errores de conexión, etc.
return nil, fmt.Errorf("error en la llamada al servicio de productos: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("servicio de productos retornó estado %d", resp.StatusCode)
}
var validationResults []ProductValidationResponseItem
if err := json.NewDecoder(resp.Body).Decode(&validationResults); err != nil {
return nil, fmt.Errorf("error al decodificar la respuesta: %w", err)
}
// Convertimos a nuestro modelo interno si es necesario, o lo usamos directamente.
// ... (conversión simplificada)
return validationResults, nil
}
type OrderService struct {
orderRepo repository.OrderRepository
productClient ProductServiceClient
}
func NewOrderService(repo repository.OrderRepository, productClient ProductServiceClient) *OrderService {
return &OrderService{orderRepo: repo, productClient: productClient}
}
func (s *OrderService) CreateOrder(req models.CreateOrderRequest) (*models.Order, error) {
// 1. Validar productos y stock llamando al otro microservicio
validationResults, err := s.productClient.ValidateProducts(req.Items)
if err != nil {
return nil, fmt.Errorf("fallo en la validación de productos: %w", err)
}
// 2. Verificar que todos los productos estén disponibles
var orderItems []models.OrderItem
total := 0.0
for i, vr := range validationResults {
if !vr.Available {
return nil, fmt.Errorf("producto %s no disponible: %s", req.Items[i].ProductID, vr.Message)
}
// Construir el ítem del pedido con la información actualizada
orderItems = append(orderItems, models.OrderItem{
ProductID: vr.ProductID,
ProductName: vr.Name,
Quantity: req.Items[i].Quantity,
UnitPrice: vr.UnitPrice,
})
total += vr.UnitPrice * float64(req.Items[i].Quantity)
}
// 3. Construir y guardar la orden
newOrder := &models.Order{
ID: generateOrderID(), // Función para generar ID único
UserID: req.UserID,
Items: orderItems,
Total: total,
Status: "PENDIENTE_DE_PAGO",
CreatedAt: time.Now(),
}
err = s.orderRepo.Save(newOrder)
if err != nil {
return nil, fmt.Errorf("error al guardar el pedido en la base de datos: %w", err)
}
return newOrder, nil
}
// Función auxiliar para generar ID (ejemplo simple)
func generateOrderID() string {
return fmt.Sprintf("ORD-%d", time.Now().UnixNano())
}
Finalmente, el handler HTTP que utiliza este servicio, en `handlers/order_handler.go`:
// handlers/order_handler.go - Microservicio de Pedidos
package handlers
import (
"encoding/json"
"net/http"
"tu-proyecto/models"
"tu-proyecto/service"
)
type OrderHandler struct {
orderService *service.OrderService
}
func NewOrderHandler(os *service.OrderService) *OrderHandler {
return &OrderHandler{orderService: os}
}
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req models.CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Cuerpo de la solicitud inválido", http.StatusBadRequest)
return
}
// Aquí iría una validación de estructura (usando go-playground/validator, por ejemplo)
order, err := h.orderService.CreateOrder(req)
if err != nil {
// Podemos diferenciar tipos de error para retornar códigos HTTP más específicos
http.Error(w, err.Error(), http.StatusUnprocessableEntity) // 422 es común para errores de negocio
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(order)
}
Errores Comunes y Cómo Evitarlos
Al implementar este patrón de microservicios, varios errores frecuentes pueden comprometer la estabilidad, rendimiento y consistencia del sistema. Identificarlos temprano es crucial.
1. Falta de Timeouts en las Llamadas HTTP entre Servicios. No configurar un timeout en el cliente HTTP del servicio de pedidos cuando llama al de productos puede llevar a un bloqueo indefinido de goroutines. Si el servicio de productos se cae o tiene una latencia enorme, los hilos del servicio de pedidos quedarán esperando, agotando eventualmente sus recursos (como el pool de conexiones) y causando un fallo en cascada. Solución: Siempre usa un `http.Client` con un `Timeout` adecuado al contexto, como se muestra en el código de ejemplo (5 segundos). Considera también timeouts por operación de lectura/escritura.
2. Manejo Inadecuado de Errores y Códigos HTTP. Simplemente propagar el error crudo de la capa de servicio al cliente HTTP puede filtrar detalles internos sensibles. Además, no diferenciar entre un error de validación de negocio (stock insuficiente) y un error de infraestructura (base de datos caída) resulta en una mala experiencia para el cliente consumidor de la API. Solución: Crea un middleware o helper para manejar errores que mapee tipos de error internos a respuestas HTTP estandarizadas (ej., 404 para no encontrado, 422 para errores de negocio, 500 para errores internos con un ID de log). Usa estructuras de error enriquecidas.
3. Acoplamiento Encubierto a través de Bases de Datos Compartidas o Dependencias Directas. Aunque separen el código en servicios distintos, un error común es que ambos servicios lean o escriban en la misma tabla de base de datos "por practicidad". Esto destruye por completo la autonomía del servicio y el desacoplamiento. Solución: Mantén una estricta separación de almacenamiento de datos. Cada servicio es dueño absoluto de su esquema de datos. La comunicación debe ser exclusivamente a través de APIs bien definidas o eventos.
4. No Considerar la Idempotencia en la Creación de Pedidos. En un entorno de red no confiable, un cliente puede reenviar la misma solicitud POST de pedido si no recibe respuesta a tiempo, arriesgándose a crear pedidos duplicados. Solución: Implementa un mecanismo de idempotencia. El cliente puede enviar un `idempotency_key` único (como un UUID) en el header de la solicitud. El servicio de pedidos debe verificar si ya procesó una solicitud con esa clave y, de ser así, retornar el mismo pedido creado anteriormente en lugar de crear uno nuevo.
5. Logging y Trazabilidad Insuficiente. Cuando un pedido falla, ¿puedes rastrear fácilmente el flujo completo a través de los logs de ambos microservicios? Sin un `request_id` o `correlation_id` que se propague en todas las llamadas internas y entre servicios, depurar problemas distribuidos se convierte en una pesadilla. Solución: Implementa middleware de logging que inyecte un ID de correlación único en el contexto de cada solicitud entrante. Asegúrate de que este ID se incluya en todos los logs y se propague en los headers de cualquier llamada HTTP saliente a otros servicios (como la llamada al servicio de productos).
Checklist de Dominio
Al finalizar esta lección, debes ser capaz de verificar tu comprensión y habilidad práctica con los siguientes puntos:
- Puedo explicar la diferencia fundamental entre una arquitectura monolítica y una basada en microservicios para un caso de e-commerce.
- He implementado un microservicio en Go con Gorilla/Mux que sigue una separación clara de capas (handlers, services, repositories, models).
- Puedo codificar un cliente HTTP robusto en Go, configurado con timeouts, para que un servicio se comunique con otro de manera síncrona.
- Sé diseñar y aplicar contratos de API (estructuras request/response JSON) claros y estables para la comunicación entre servicios.
- Puedo manejar errores de comunicación entre servicios (timeouts, códigos de estado no exitosos) y traducirlos a respuestas HTTP apropiadas para el cliente final.
- Comprendo el riesgo de las condiciones de carrera en operaciones como la validación de stock y puedo mencionar al menos una estrategia para mitigarlo (transacciones, bloqueo optimista).
- He considerado la importancia de la idempotencia en endpoints de creación (como POST /orders) y sé cómo podría implementarla.
- Puedo describir la importancia de la trazabilidad distribuida (correlation IDs) y cómo implementarla en un flujo que cruza múltiples servicios.