Diseño de la arquitectura y planificación del proyecto

Lectura
30 min~10 min lectura
Objetivo de la lección

Lección: Diseño de la arquitectura y planificación del proyecto Diseño de la arquitectura y planificación del proyecto Bienvenido a la lección fundacional del proyecto integrador.

Puntos de control
  • Definición de los límites de los microservicios
  • Concepto clave: Arquitectura basada en eventos y comunicación asíncrona
  • Cómo funciona en la práctica: Flujo de una compra
  • Código en acción: Estructura del proyecto y configuración inicial
Lección: Diseño de la arquitectura y planificación del proyecto

Diseño de la arquitectura y planificación del proyecto

Bienvenido a la lección fundacional del proyecto integrador. Antes de escribir una sola línea de código en Go, debemos dedicar tiempo a diseñar una arquitectura sólida y escalable. Esta fase es crucial para el éxito de cualquier sistema de microservicios, especialmente uno destinado a un entorno de e-commerce con demandas de alto rendimiento. En esta lección, definiremos los límites de nuestros servicios, sus responsabilidades, cómo se comunicarán y cómo desplegaremos y monitorizaremos el sistema completo. Un buen diseño previene problemas de acoplamiento, cuellos de botella y dificultades de mantenimiento en el futuro.

Abordaremos este diseño desde una perspectiva práctica, tomando decisiones basadas en las mejores prácticas para APIs de alto rendimiento con Go. Utilizaremos el enrutador Gorilla/Mux por su flexibilidad y rendimiento, pero la arquitectura que definamos será lo suficientemente genérica como para ser aplicada con otras herramientas. Planificaremos un sistema que incluya servicios para catálogo de productos, gestión de usuarios, carrito de compras, procesamiento de pedidos y un API Gateway para unificar el acceso. La planificación también incluye la definición de contratos de comunicación (APIs), estrategias de persistencia de datos y consideraciones sobre resiliencia.

Definición de los límites de los microservicios

El primer y más crítico paso es la descomposición del dominio de negocio (e-commerce) en servicios cohesivos e independientes. Un error común es crear servicios demasiado finos (nanoservicios) o demasiado gruesos (minimonolitos). Nuestro objetivo es identificar límites contextuales claros. Para un e-commerce básico, podemos identificar los siguientes contextos centrales: Catálogo (productos, categorías, inventario), Usuarios (autenticación, perfiles, direcciones), Carrito (sesiones de compra temporal), Pedidos (procesamiento y historial) y Pagos (integración con pasarelas). Cada uno de estos contextos tendrá su propio ciclo de vida de datos y lógica específica.

Es fundamental que cada servicio sea dueño de sus datos. El servicio de Catálogo gestionará la base de datos de productos; el servicio de Usuarios, la de clientes; y así sucesivamente. Esto evita el acoplamiento de datos y permite elegir la tecnología de almacenamiento más adecuada para cada caso (por ejemplo, PostgreSQL para relaciones complejas en Pedidos y Redis para sesiones de Carrito). La comunicación entre servicios para operaciones que requieren datos de múltiples contextos se realizará exclusivamente a través de sus APIs HTTP bien definidas, nunca mediante acceso directo a la base de datos de otro servicio.

Tip del Arquitecto: Una buena heurística para definir un límite de servicio es preguntarse: "¿Puede este conjunto de funcionalidades desplegarse, escalar y fallar de manera independiente sin impactar críticamente a los demás?" Si la respuesta es sí, estás en el camino correcto.

Concepto clave: Arquitectura basada en eventos y comunicación asíncrona

Imagina una cocina de restaurante de alta gama. El cliente (cliente frontend) hace un pedido al maître (API Gateway). El maître no cocina, sino que pasa la comanda (solicitud HTTP) a la estación correspondiente: carnes, pescados, postres (microservicios). Pero lo más interesante ocurre detrás: cuando la estación de carnes termina un filete, no lleva él mismo el plato al cliente. Coloca el plato terminado en el carrito de "comidas listas" (cola de eventos) y avisa con un timbre (publica un evento). El camarero (servicio de Orquestación o otro servicio suscriptor) escucha ese timbre, recoge el plato y lo sirve. Esta es la esencia de la comunicación asíncrona basada en eventos.

En nuestro sistema, no todos los servicios necesitan respuestas inmediatas de otros. Por ejemplo, cuando se confirma un pedido, el servicio de Pedidos debe actualizar su estado y avisar a otros sistemas. Debe notificar al servicio de Catálogo para que reserve/descuente el inventario, y al servicio de Usuarios para que actualice el historial del cliente. Si hacemos esto con llamadas HTTP síncronas en cadena, la confirmación del pedido será lenta y frágil (si el servicio de Catálogo está lento, el cliente espera). En su lugar, el servicio de Pedidos publicará un evento PedidoConfirmado a un message broker (como RabbitMQ o Apache Kafka). Los servicios de Catálogo y Usuarios, suscritos a ese tipo de evento, lo procesarán a su propio ritmo, desacoplando la cadena y mejorando la resiliencia.

Cómo funciona en la práctica: Flujo de una compra

Vamos a trazar el flujo completo de una operación clave: un usuario finalizando una compra. Este ejemplo ilustra la interacción entre nuestros servicios. 1) El cliente web/app envía una solicitud POST a `/api/checkout`. Esta solicitud es capturada por el API Gateway, que enruta la petición al servicio de Carrito basándose en la ruta. 2) El servicio de Carrito recibe el ID del carrito y del usuario, valida los items y llama síncronamente al servicio de Pedidos para crear un pedido en estado "Pendiente de Pago", enviando los datos del carrito. 3) El servicio de Pedidos persiste el pedido y devuelve un ID de pedido y un total.

4) El servicio de Carrito, con esa información, redirige al cliente al servicio de Pagos (o devuelve los datos para que el frontend lo haga). 5) Tras un pago exitoso, el servicio de Pagos llama a un endpoint del servicio de Pedidos (ej: PUT /api/orders/{id}/confirm). 6) Aquí es donde entra la magia asíncrona. El servicio de Pedidos, al confirmar el pedido, cambia su estado a "Procesando" y publica un evento OrderConfirmed a la cola de mensajes. Este evento contiene el ID del pedido y la lista de productos comprados. 7) El servicio de Catálogo, escuchando esa cola, recibe el evento y actualiza el inventario de cada producto. Paralelamente, el servicio de Usuarios puede recibir el mismo evento para actualizar el historial de compras del cliente. El cliente ha recibido ya una confirmación rápida, mientras el sistema procesa las tareas posteriores en segundo plano.

Código en acción: Estructura del proyecto y configuración inicial

Antes de los servicios, definamos la estructura de carpetas y un componente compartido: la configuración. Usaremos un enfoque basado en variables de entorno y un paquete `config`. Esto asegura coherencia en todos los servicios.

// File: /pkg/config/config.go
package config

import (
    "os"
    "strconv"
)

type Config struct {
    ServerPort string
    DBHost     string
    DBPort     string
    DBUser     string
    DBPassword string
    DBName     string
    RabbitMQURL string
    JWTSecret   string
}

func Load() *Config {
    return &Config{
        ServerPort: getEnv("SERVER_PORT", "8080"),
        DBHost:     getEnv("DB_HOST", "localhost"),
        DBPort:     getEnv("DB_PORT", "5432"),
        DBUser:     getEnv("DB_USER", "postgres"),
        DBPassword: getEnv("DB_PASSWORD", ""),
        DBName:     getEnv("DB_NAME", "catalog_db"),
        RabbitMQURL: getEnv("RABBITMQ_URL", "amqp://guest:guest@localhost:5672/"),
        JWTSecret:   getEnv("JWT_SECRET", "supersecretkey"),
    }
}

func getEnv(key, defaultValue string) string {
    if value, exists := os.LookupEnv(key); exists {
        return value
    }
    return defaultValue
}

Ahora, veamos la estructura de directorios raíz del proyecto. Cada servicio vive en su propio directorio, replicando una estructura interna similar.


ecommerce-microservices/
├── api-gateway/
│   ├── cmd/
│   │   └── main.go
│   ├── internal/
│   │   ├── handler/
│   │   └── router.go
│   └── go.mod
├── service-catalog/
│   ├── cmd/
│   │   └── main.go
│   ├── internal/
│   │   ├── handler/
│   │   ├── repository/
│   │   └── service/
│   ├── pkg/
│   │   └── models/
│   └── go.mod
├── service-users/
│   └── ... # Estructura similar
├── service-cart/
│   └── ...
├── service-orders/
│   └── ...
├── service-payments/
│   └── ...
├── pkg/
│   ├── config/       # Código compartido como la configuración
│   └── events/       # Estructuras de eventos compartidos
├── docker-compose.yml
└── README.md

Finalmente, aquí hay un esqueleto del `main.go` para el servicio de catálogo, mostrando la inicialización con Gorilla/Mux y la configuración cargada.

// File: /service-catalog/cmd/main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gorilla/mux"
    "ecommerce-microservices/pkg/config"
    "ecommerce-microservices/service-catalog/internal/handler"
    "ecommerce-microservices/service-catalog/internal/repository"
    "ecommerce-microservices/service-catalog/internal/service"
)

func main() {
    // Cargar configuración
    cfg := config.Load()

    // Inicializar dependencias (Repositorio -> Servicio -> Handler)
    productRepo := repository.NewProductRepository(cfg) // Asume implementación
    productService := service.NewProductService(productRepo)
    productHandler := handler.NewProductHandler(productService)

    // Configurar router Gorilla/Mux
    r := mux.NewRouter()
    api := r.PathPrefix("/api/v1").Subrouter()

    // Rutas
    api.HandleFunc("/products", productHandler.GetProducts).Methods("GET")
    api.HandleFunc("/products/{id}", productHandler.GetProduct).Methods("GET")
    api.HandleFunc("/products", productHandler.CreateProduct).Methods("POST")
    // ... más rutas

    // Configurar servidor HTTP
    srv := &http.Server{
        Addr:         ":" + cfg.ServerPort,
        Handler:      r,
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    // Ejecutar servidor en goroutine
    go func() {
        log.Printf("Servicio Catálogo iniciado en el puerto %s", cfg.ServerPort)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Error al iniciar el servidor: %v", err)
        }
    }()

    // Esperar señal de interrupción para un apagado graceful
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
    <-stop

    log.Println("Apagando el servidor...")
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Error en el apagado forzoso: %v", err)
    }
    log.Println("Servicio detenido correctamente.")
}

Errores comunes y cómo evitarlos

Al diseñar y planificar un sistema de microservicios, varios errores pueden comprometer el proyecto desde el inicio.

1. Diseñar servicios acoplados por la base de datos: El error más grave es permitir que múltiples servicios lean y escriban directamente en la misma base de datos. Esto crea un acoplamiento oculto y frágil. Solución: Aplica la regla "Un servicio, una base de datos". Cada servicio gestiona su propio esquema y solo expone datos a través de su API. Para datos compartidos, usa replicación de datos vía eventos o un patrón "Materialized View".

2. Ignorar la idempotencia en las comunicaciones: En un entorno distribuido y asíncrono, los mensajes o solicitudes pueden duplicarse. Si el procesamiento de un evento `PedidoConfirmado` no es idempotente, podrías descontar el inventario dos veces. Solución: Diseña tus endpoints y handlers de eventos para ser idempotentes. Usa IDs únicos (como un `eventId` o `requestId`) y verifica si ya has procesado esa operación antes de ejecutarla.

3. Subestimar la complejidad operativa: Creer que microservicios son solo dividir código, sin planificar el despliegue, monitorización, logging centralizado y trazabilidad. Solución: Incluye desde el día 1 herramientas como Docker, Docker Compose (para desarrollo), un plan para logs agregados (ELK Stack o similar) y tracing distribuido (Jaeger o OpenTelemetry). Define métricas de salud para cada servicio.

4. No definir contratos de API claros y versionados: Cambiar la API de un servicio sin control romperá a los servicios que dependen de él. Solución: Usa especificaciones OpenAPI (Swagger) para documentar cada API. Siempre versiona tus endpoints (ej: `/api/v1/products`). Implementa estrategias de versionado como URL versioning o header versioning y comunica los cambios.

5. Crear una cadena síncrona de llamadas HTTP: Conectar servicios en una larga cadena síncrona (A llama a B, que llama a C, que espera a D...) resulta en una latencia inaceptable y un punto único de fallo en cascada. Solución: Identifica operaciones que pueden ser asíncronas. Usa colas de mensajes para desacoplar procesos de larga duración o que no son necesarios para la respuesta inmediata al cliente, como vimos en el flujo de compra.

Checklist de dominio

Antes de proceder a la implementación de cada servicio, verifica que has completado y comprendido los siguientes puntos de planificación y diseño:

  • He definido y documentado los límites de al menos 4-5 microservicios principales (ej: Catálogo, Usuarios, Carrito, Pedidos) con sus responsabilidades únicas.
  • He diseñado el esquema de base de datos independiente para cada servicio y he elegido la tecnología de almacenamiento apropiada (SQL vs NoSQL).
  • He definido los contratos de API (endpoints, métodos, request/response bodies) para cada servicio, preferiblemente usando un formato como OpenAPI.
  • He identificado al menos dos flujos de negocio que se beneficiarán de comunicación asíncrona basada en eventos (ej: confirmación de pedido, envío de email de bienvenida) y he esbozado los eventos implicados.
  • He planificado la infraestructura básica: cómo se ejecutarán los servicios en desarrollo (Docker Compose), y he considerado necesidades de producción (orquestador, message broker).
  • He establecido una estrategia para la gestión de configuraciones, secretos y logs centralizados para todo el sistema.
  • He definido un plan para el manejo de errores y la resiliencia, considerando timeouts, retries y circuit breakers en las comunicaciones entre servicios.
  • He diseñado la estrategia de autenticación y autorización a nivel del API Gateway y entre servicios (uso de JWT, API Keys para servicios internos).
Falar no WhatsApp
Laboratorio de práctica

Antes de marcar esta lección como completa, escribí una evidencia breve para Go para APIs de Alto Rendimiento: Construcción de Microservicios REST con Gorilla/Mux: un ejemplo, una decisión, una captura, una mini demo o una nota que puedas reutilizar en portfolio.

Reflexión rápida

¿Qué cambiarías en tu forma de trabajar después de aplicar diseño de la arquitectura y planificación del proyecto?

De lección a portfolio

Convertí esta lección en una prueba técnica visible.

Una app pequeña publicada, con README y decisiones explicadas, funciona mejor que una lista de tecnologías sueltas.

Paso 1

Creá una demo mínima que use el concepto de la lección.

Paso 2

Escribí un README corto con objetivo, stack, decisión técnica y mejora futura.

Paso 3

Publicá la demo y enlazala desde tu perfil profesional.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión

Diseño de la arquitectura y planificación del p... | Cursalo