Arquitectura de Software

Arquitectura Hexagonal en Flask: Cambia de Proveedor IA

Aprende a implementar arquitectura hexagonal en Flask con un puerto para IA. Ejemplo real con código: cambia entre OpenAI y Claude sin tocar el dominio.

La arquitectura hexagonal (también conocida como Ports & Adapters) suele explicarse en teoría, pero pocas veces se muestra con un ejemplo tangible. En este artículo vamos a construir, paso a paso, una aplicación en Flask que expone un puerto para consumir distintos proveedores de IA (OpenAI, Anthropic/Claude, o cualquier otro) sin que el dominio de la aplicación dependa de ninguno de ellos en particular.

El objetivo es demostrar empíricamente que, si el diseño está bien hecho, cambiar de proveedor de IA debería ser tan simple como cambiar una línea de configuración, sin tocar la lógica de negocio.

¿Qué es la arquitectura hexagonal y por qué aplicarla en Flask?

La arquitectura hexagonal propone separar el núcleo de dominio (las reglas de negocio) de los detalles técnicos (frameworks, bases de datos, APIs externas). Esa separación se logra mediante puertos (interfaces que definen qué necesita el dominio) y adaptadores (implementaciones concretas que satisfacen esos puertos).

Ya habíamos analizado este patrón en profundidad al estudiar cómo Netflix lo aplica en producción.

Artículo Relacionado: La ingeniería detrás de Netflix: su arquitectura hexagonal

Flask es un framework minimalista, lo cual lo hace ideal para este ejercicio: no impone una estructura de carpetas ni un ORM específico, así que nosotros decidimos exactamente dónde viven el dominio, los puertos y los adaptadores.

El problema: acoplar tu aplicación a un proveedor de IA específico

Es común ver código Flask donde una ruta llama directamente al SDK de OpenAI o de Anthropic. Funciona, pero genera un acoplamiento fuerte: si mañana quieres migrar a otro proveedor, comparar costos, o hacer fallback entre modelos, terminas modificando la lógica de negocio mezclada con detalles de infraestructura.

Este es exactamente el tipo de problema que los principios SOLID —en particular la inversión de dependencias— buscan resolver.

Artículo Relacionado: Principios SOLID con Python: Single Responsibility

La solución: un puerto para el proveedor de IA

Definimos una interfaz abstracta que representa “lo que el dominio necesita de un proveedor de IA”, sin saber nada sobre HTTP, tokens de API ni formatos de respuesta propietarios.

# domain/ports/ai_provider_port.py
from abc import ABC, abstractmethod

class AIProviderPort(ABC):
    @abstractmethod
    def generate_response(self, prompt: str, system: str | None = None) -> str:
        """Genera una respuesta de texto a partir de un prompt."""
        raise NotImplementedError

Este puerto vive en el dominio y no importa ni Flask, ni requests, ni ningún SDK externo. Es Python puro.

Implementando la arquitectura hexagonal en Flask paso a paso

Con el puerto definido, la estructura de carpetas del proyecto queda así:

proyecto/
├── domain/
│   ├── ports/
│   │   └── ai_provider_port.py
│   └── services/
│       └── chat_service.py
├── adapters/
│   ├── driven/
│   │   ├── openai_adapter.py
│   │   └── anthropic_adapter.py
│   └── driving/
│       └── flask_routes.py
├── config.py
└── app.py

El caso de uso (servicio de dominio) solo conoce el puerto, nunca una implementación concreta:

# domain/services/chat_service.py
from domain.ports.ai_provider_port import AIProviderPort

class ChatService:
    def __init__(self, ai_provider: AIProviderPort):
        self._ai_provider = ai_provider

    def responder(self, pregunta: str) -> str:
        system_prompt = "Eres un asistente técnico conciso."
        return self._ai_provider.generate_response(pregunta, system=system_prompt)

Fíjate que ChatService recibe el puerto por inyección de dependencias en el constructor. No sabe, ni le importa, si detrás hay OpenAI, Claude o un modelo local.

Adaptadores concretos: OpenAI y Anthropic Claude

Ahora implementamos dos adaptadores que cumplen el contrato del puerto. Si ya integraste Claude en un proyecto Flask anteriormente, esta parte te resultará muy familiar.

Artículo Relacionado: Integración de Claude API con Flask: tutorial paso a paso

# adapters/driven/anthropic_adapter.py
import anthropic
from domain.ports.ai_provider_port import AIProviderPort

class AnthropicAdapter(AIProviderPort):
    def __init__(self, api_key: str, model: str = "claude-sonnet-4-6"):
        self._client = anthropic.Anthropic(api_key=api_key)
        self._model = model

    def generate_response(self, prompt: str, system: str | None = None) -> str:
        message = self._client.messages.create(
            model=self._model,
            max_tokens=500,
            system=system or "",
            messages=[{"role": "user", "content": prompt}],
        )
        return message.content[0].text
# adapters/driven/openai_adapter.py
from openai import OpenAI
from domain.ports.ai_provider_port import AIProviderPort

class OpenAIAdapter(AIProviderPort):
    def __init__(self, api_key: str, model: str = "gpt-4o"):
        self._client = OpenAI(api_key=api_key)
        self._model = model

    def generate_response(self, prompt: str, system: str | None = None) -> str:
        messages = []
        if system:
            messages.append({"role": "system", "content": system})
        messages.append({"role": "user", "content": prompt})

        completion = self._client.chat.completions.create(
            model=self._model,
            messages=messages,
        )
        return completion.choices[0].message.content

Ambos adaptadores implementan exactamente el mismo método generate_response. Desde la perspectiva del dominio, son intercambiables.

El adaptador Flask: exponiendo el caso de uso vía HTTP

Flask actúa como adaptador de entrada (driving adapter): traduce una petición HTTP en una llamada al caso de uso del dominio.

# adapters/driving/flask_routes.py
from flask import Blueprint, request, jsonify
from domain.services.chat_service import ChatService

chat_bp = Blueprint("chat", __name__)

def init_chat_routes(chat_service: ChatService):
    @chat_bp.route("/api/chat", methods=["POST"])
    def chat():
        data = request.get_json()
        pregunta = data.get("pregunta", "")
        respuesta = chat_service.responder(pregunta)
        return jsonify({"respuesta": respuesta})

    return chat_bp

La ruta no sabe nada de OpenAI ni de Anthropic; solo conversa con ChatService, que a su vez conversa con el puerto. Si ya construiste un mantenedor CRUD con Flask, notarás que el patrón de blueprints es el mismo, solo que aquí lo aprovechamos para mantener la separación de capas.

Artículo Relacionado: Tutorial de Flask con Python: crear un mantenedor

Composición: el punto donde todo se conecta

El único lugar donde el dominio “se entera” de qué adaptador se está usando es en el punto de entrada de la aplicación, mediante configuración:

# app.py
import os
from flask import Flask
from adapters.driving.flask_routes import init_chat_routes
from adapters.driven.anthropic_adapter import AnthropicAdapter
from adapters.driven.openai_adapter import OpenAIAdapter
from domain.services.chat_service import ChatService

def create_app():
    app = Flask(__name__)

    proveedor = os.getenv("AI_PROVIDER", "anthropic")

    if proveedor == "openai":
        ai_provider = OpenAIAdapter(api_key=os.getenv("OPENAI_API_KEY"))
    else:
        ai_provider = AnthropicAdapter(api_key=os.getenv("ANTHROPIC_API_KEY"))

    chat_service = ChatService(ai_provider)
    app.register_blueprint(init_chat_routes(chat_service))

    return app

if __name__ == "__main__":
    create_app().run(debug=True)

Cambiar de proveedor es literalmente cambiar una variable de entorno. No se toca el dominio, no se toca la ruta Flask, y ningún test unitario del caso de uso necesita reescribirse.

Ventajas demostradas empíricamente con este ejemplo

  • Testabilidad: puedes crear un FakeAIProvider que implemente el puerto y probar ChatService sin llamar a ninguna API real.
  • Flexibilidad de proveedor: agregar un tercer adaptador (por ejemplo, un modelo local con Ollama) no requiere modificar el dominio.
  • Resiliencia: se puede envolver el puerto en un adaptador con lógica de fallback entre proveedores, algo especialmente útil si además trabajas con búsqueda semántica y bases de datos vectoriales.

Artículo Relacionado: Bases de datos vectoriales en Python

Este mismo patrón de puertos y adaptadores es, en esencia, el que sostiene arquitecturas de microservicios más grandes, donde cada servicio expone contratos claros hacia el exterior.

Artículo Relacionado: ¿Qué son y para qué sirven los microservicios?

Conclusión

Construir un ejemplo concreto de arquitectura hexagonal en Flask con un puerto para proveedores de IA no es solo un ejercicio académico: es una defensa práctica contra el vendor lock-in y un camino directo hacia código más testeable y mantenible. Si te interesa profundizar en el porqué de este patrón antes de aplicarlo, vale la pena revisar también su primo cercano.

Artículo Relacionado: ¿Qué es Clean Architecture y cuáles son sus beneficios y desventajas?

Para profundizar en los detalles técnicos de cada SDK, puedes revisar la documentación oficial de Flask, de la API de OpenAI y de la API de Anthropic, así como el módulo abc de Python, que es la base para definir los puertos como clases abstractas.

Comparte este artículo
Juanjo González

Recent Posts

Microservicios: Guía Completa 2026 (IA, Service Mesh y Más)

La arquitectura de microservicios 2026 es, el estándar de facto para construir sistemas escalables y…

1 día ago

6 Casos de Uso de Bases de Datos Vectoriales con Python

Las bases de datos vectoriales dejaron de ser un concepto de laboratorio para convertirse en…

1 día ago

Base de Datos Vectorial en Python: Guía Completa para Proyectos de IA

Si trabajas con inteligencia artificial y modelos de lenguaje, tarde o temprano necesitarás almacenar y…

3 días ago

Construí un plugin de WordPress que entiende de qué trata tu contenido (no solo sus tags)

La mayoría de plugins de related posts solo comparan tags, no el significado. Te muestro…

3 días ago

Git: El “Control + Z” que Todo Programador Necesita (Y las Nuevas Herramientas que Vienen a Mejorarlo)

Git para principiantes: Imagina que llevas tres días trabajando en una nueva función para tu…

2 meses ago

Integración de Claude API con Python Flask: Tutorial Paso a Paso

Aprende a integrar la API de Claude con Python Flask paso a paso para crear…

3 meses ago