DEV Community

Enterprise Design Patterns: The Repository Pattern in Practice

Curso: Patrones de Software
Basado en: Catalog of Patterns of Enterprise Application Architecture, Martin Fowler


1. Introducción

Martin Fowler, en su libro Patterns of Enterprise Application Architecture (2002), documentó un conjunto de soluciones recurrentes a problemas que aparecen una y otra vez al construir software empresarial: cómo organizar la lógica de negocio, cómo mapear objetos a bases de datos, cómo estructurar la capa de presentación, entre otros. Su catálogo online agrupa estos patrones en categorías como Domain Logic Patterns, Data Source Architectural Patterns, Object-Relational Behavioral Patterns, etc.

En este artículo desarrollo el patrón Repository, perteneciente a la categoría de Object-Relational Behavioral Patterns, y lo acompaño con una implementación real en Python.

2. ¿Qué es el patrón Repository?

Fowler define el Repository como una capa que media entre el dominio y la capa de acceso a datos, comportándose como una colección de objetos en memoria. La idea central es que el resto de la aplicación (los casos de uso, los servicios, los controladores) no debería saber si los datos vienen de una base de datos SQL, de un archivo, de una API externa o de una lista en memoria. El Repository oculta esa complejidad detrás de una interfaz simple: add(), get(), list(), remove().

Problema que resuelve

Sin este patrón, es común ver consultas SQL o código de ORM esparcido por toda la lógica de negocio. Esto genera:

  • Acoplamiento fuerte entre la lógica de negocio y la tecnología de persistencia.
  • Dificultad para probar el código (hay que levantar una base de datos real para cada test).
  • Duplicación de queries similares en distintos puntos del sistema.

Solución

Se define una interfaz abstracta (un contrato) que declara las operaciones disponibles sobre una colección de objetos de dominio, y luego se implementan una o varias versiones concretas de esa interfaz (por ejemplo, una para SQLite, otra en memoria para pruebas, otra para MongoDB, etc.). El resto del sistema solo depende de la interfaz, nunca de la implementación concreta.

3. Ejemplo real de código (Python)

El ejemplo modela un sistema simple de gestión de pedidos (Orders) para una tienda. Se define:

  1. Una entidad de dominio Order.
  2. Una interfaz abstracta OrderRepository.
  3. Dos implementaciones: InMemoryOrderRepository (para pruebas unitarias) y SqliteOrderRepository (para producción).
  4. Un caso de uso (OrderService) que depende únicamente de la interfaz.

3.1 Entidad de dominio

# src/domain.py
from dataclasses import dataclass, field
from datetime import datetime
from uuid import uuid4


@dataclass
class Order:
    customer_name: str
    total_amount: float
    id: str = field(default_factory=lambda: str(uuid4()))
    created_at: datetime = field(default_factory=datetime.utcnow)

    def apply_discount(self, percentage: float) -> None:
        if not 0 <= percentage <= 100:
            raise ValueError("El descuento debe estar entre 0 y 100")
        self.total_amount -= self.total_amount * (percentage / 100)
Enter fullscreen mode Exit fullscreen mode

3.2 Interfaz del Repository (el contrato)

# src/repository.py
from abc import ABC, abstractmethod
from typing import List, Optional

from .domain import Order


class OrderRepository(ABC):
    """Contrato que toda implementación de persistencia debe cumplir.
    La lógica de negocio solo conoce esta interfaz, nunca los detalles
    de SQLite, memoria, MongoDB, etc.
    """

    @abstractmethod
    def add(self, order: Order) -> None: ...

    @abstractmethod
    def get(self, order_id: str) -> Optional[Order]: ...

    @abstractmethod
    def list_all(self) -> List[Order]: ...

    @abstractmethod
    def remove(self, order_id: str) -> None: ...
Enter fullscreen mode Exit fullscreen mode

3.3 Implementación en memoria (ideal para tests)

# src/in_memory_repository.py
from typing import Dict, List, Optional

from .domain import Order
from .repository import OrderRepository


class InMemoryOrderRepository(OrderRepository):
    def __init__(self) -> None:
        self._orders: Dict[str, Order] = {}

    def add(self, order: Order) -> None:
        self._orders[order.id] = order

    def get(self, order_id: str) -> Optional[Order]:
        return self._orders.get(order_id)

    def list_all(self) -> List[Order]:
        return list(self._orders.values())

    def remove(self, order_id: str) -> None:
        self._orders.pop(order_id, None)
Enter fullscreen mode Exit fullscreen mode

3.4 Implementación con SQLite (persistencia real)

# src/sqlite_repository.py
import sqlite3
from datetime import datetime
from typing import List, Optional

from .domain import Order
from .repository import OrderRepository


class SqliteOrderRepository(OrderRepository):
    def __init__(self, db_path: str = "orders.db") -> None:
        self._conn = sqlite3.connect(db_path)
        self._create_table()

    def _create_table(self) -> None:
        self._conn.execute(
            """
            CREATE TABLE IF NOT EXISTS orders (
                id TEXT PRIMARY KEY,
                customer_name TEXT NOT NULL,
                total_amount REAL NOT NULL,
                created_at TEXT NOT NULL
            )
            """
        )
        self._conn.commit()

    def add(self, order: Order) -> None:
        self._conn.execute(
            "INSERT OR REPLACE INTO orders VALUES (?, ?, ?, ?)",
            (order.id, order.customer_name, order.total_amount,
             order.created_at.isoformat()),
        )
        self._conn.commit()

    def get(self, order_id: str) -> Optional[Order]:
        row = self._conn.execute(
            "SELECT id, customer_name, total_amount, created_at FROM orders WHERE id = ?",
            (order_id,),
        ).fetchone()
        return self._row_to_order(row) if row else None

    def list_all(self) -> List[Order]:
        rows = self._conn.execute(
            "SELECT id, customer_name, total_amount, created_at FROM orders"
        ).fetchall()
        return [self._row_to_order(row) for row in rows]

    def remove(self, order_id: str) -> None:
        self._conn.execute("DELETE FROM orders WHERE id = ?", (order_id,))
        self._conn.commit()

    @staticmethod
    def _row_to_order(row) -> Order:
        return Order(
            id=row[0],
            customer_name=row[1],
            total_amount=row[2],
            created_at=datetime.fromisoformat(row[3]),
        )
Enter fullscreen mode Exit fullscreen mode

3.5 Caso de uso desacoplado de la persistencia

# src/service.py
from .domain import Order
from .repository import OrderRepository


class OrderService:
    """Depende solo de la abstracción OrderRepository.
    No sabe -ni le importa- si los pedidos viven en SQLite o en memoria.
    """

    def __init__(self, repository: OrderRepository) -> None:
        self._repository = repository

    def place_order(self, customer_name: str, total_amount: float) -> Order:
        order = Order(customer_name=customer_name, total_amount=total_amount)
        self._repository.add(order)
        return order

    def apply_discount_to_order(self, order_id: str, percentage: float) -> Order:
        order = self._repository.get(order_id)
        if order is None:
            raise ValueError(f"Pedido {order_id} no encontrado")
        order.apply_discount(percentage)
        self._repository.add(order)  # actualiza
        return order
Enter fullscreen mode Exit fullscreen mode

3.6 Demostración: intercambiando implementaciones sin tocar la lógica

# main.py
from src.in_memory_repository import InMemoryOrderRepository
from src.sqlite_repository import SqliteOrderRepository
from src.service import OrderService


def run_demo(repository):
    service = OrderService(repository)
    order = service.place_order("Ariana Quispe", 250.00)
    print(f"Pedido creado: {order}")

    updated = service.apply_discount_to_order(order.id, 10)
    print(f"Pedido con descuento: {updated}")

    print("Todos los pedidos:", service._repository.list_all())


if __name__ == "__main__":
    print("--- Usando repositorio en memoria ---")
    run_demo(InMemoryOrderRepository())

    print("\n--- Usando repositorio SQLite (persistente) ---")
    run_demo(SqliteOrderRepository("orders.db"))
Enter fullscreen mode Exit fullscreen mode

Punto clave del ejemplo: OrderService nunca cambia. Basta con inyectarle un repositorio distinto (InMemoryOrderRepository o SqliteOrderRepository) para cambiar completamente la forma en que se persisten los datos. Esto es exactamente el beneficio que describe Fowler: la lógica de dominio queda aislada de los detalles de infraestructura.

4. Pruebas unitarias (ventaja directa del patrón)

# tests/test_order_service.py
import pytest
from src.in_memory_repository import InMemoryOrderRepository
from src.service import OrderService


def test_place_order_stores_it_in_repository():
    repo = InMemoryOrderRepository()
    service = OrderService(repo)

    order = service.place_order("Gustavo Mamani", 100.0)


    assert repo.get(order.id) is not None
    assert repo.get(order.id).customer_name == "Gustavo Mamani"


def test_apply_discount_updates_total_amount():
    repo = InMemoryOrderRepository()
    service = OrderService(repo)
    order = service.place_order("Hashira", 200.0)

    updated = service.apply_discount_to_order(order.id, 25)

    assert updated.total_amount == 150.0
Enter fullscreen mode Exit fullscreen mode

Gracias a la interfaz OrderRepository, estas pruebas corren en milisegundos, sin necesidad de una base de datos real — otro beneficio directo que Fowler atribuye al patrón.

5. Relación con otros patrones del catálogo

  • Data Mapper: el Repository suele apoyarse en un Data Mapper para traducir entre el objeto de dominio y las filas de la base de datos.
  • Unit of Work: en sistemas más grandes, el Repository trabaja junto a un Unit of Work que agrupa varias operaciones en una sola transacción.
  • Service Layer: OrderService en este ejemplo cumple el rol de Service Layer, orquestando el dominio y delegando la persistencia al Repository.

6. Conclusión

El patrón Repository es una de las herramientas más usadas hoy en arquitecturas limpias (Clean Architecture, Hexagonal Architecture) precisamente porque resuelve un problema muy concreto: desacoplar el "qué" (lógica de negocio) del "cómo" (tecnología de persistencia). El ejemplo en Python demuestra que, con una interfaz bien definida, es trivial cambiar de una implementación en memoria a una persistente sin tocar una sola línea de la lógica de negocio.

Repository https://github.com/Hashiravc/repository-pattern.git

Referencias

Top comments (0)