Criteria: filtros dinámicos, paginación y DDD
Cómo el patrón Criteria + Specification resuelve el problema de los filtros dinámicos en repositorios sin contaminar el dominio con infraestructura. Ejemplo real en Code Finances.
Cuando trabajas en aplicaciones reales, hay un momento inevitable: necesitas hacer búsquedas complejas.
"Quiero buscar usuarios por rol, estado, nombre… añadir ordenación… paginar resultados… y que además el código sea limpio."
Y ahí es donde muchas arquitecturas empiezan a deteriorarse.
⚠️ El problema
En cuanto empiezas a añadir filtros dinámicos en tus repositorios:
- 🧩 Aparecen
ifpor todas partes - 💣 Cada caso acaba teniendo su propia query personalizada
- 🔄 Se pierde reutilización
- 🧠 La lógica de negocio se mezcla con detalles de infraestructura
El resultado: código difícil de mantener, extender y testear.
🚀 La solución: Criteria + Specification Pattern
✅ Criteria
Un objeto que encapsula completamente una búsqueda: filtros, ordenación y paginación.
🧱 Specification Pattern
Cada filtro se modela como una specification: independiente, reutilizable y componible.
🔗 Composición dinámica
Puedes combinar filtros sin modificar el repositorio:
criteria = new Criteria(filters, order, pageSize, pageNumber)
🎯 Resultado
El repositorio deja de "pensar" y solo ejecuta:
repository.match(criteria)
flowchart TB
subgraph Dominio
direction TB
F1[Filter: role=admin]
F2[Filter: status=active]
O[Order: name ASC]
P[Page: 1 / size: 20]
C[Criteria]
F1 --> C
F2 --> C
O --> C
P --> C
end
subgraph Infraestructura
direction TB
R[Repository.match]
QB[QueryBuilder]
DB[(PostgreSQL)]
R --> QB
QB --> DB
end
C --> R
style C fill:#1e1b4b,stroke:#818cf8,color:#e0e7ff
style R fill:#0f172a,stroke:#818cf8,color:#e0e7ff
style QB fill:#0f172a,stroke:#818cf8,color:#e0e7ff
style DB fill:#1e1b4b,stroke:#818cf8,color:#e0e7ff
⚡️ Implementación DDD-friendly
📦 Shared Domain
Aquí vive el core reutilizable entre todos los contextos:
CriteriaFilters→Field,Operator,ValueOrder→OrderBy,OrderTypePageSize(Value Object)PageNumber(Value Object)
🧩 Interfaz de repositorio en el dominio
Clave: el dominio no sabe nada de SQL, ORM ni infraestructura.
interface ContextRepository {
match(criteria: Criteria): Promise<ContextAggregate[]>
}
🏆 Ejemplo real: Code Finances
Así se organiza en el proyecto Code Finances aplicando Screaming Architecture:
Criteria.ts
Filters.ts · Field.ts · Operator.ts · Value.ts
Order.ts · OrderBy.ts · OrderType.ts
PageSize.ts · PageNumber.ts
CashFlow/Revenue/Domain/
RevenueRepository.ts ← interface match(criteria)
CashFlow/Revenue/Infra/
TypeOrmRevenueRepository.ts ← implementa la interfaz
CashFlow/Revenue/App/Search/
RevenuesSearcher.ts ← caso de uso, construye el Criteria
🧠 Beneficios reales
- ✅ Código limpio y mantenible
- ♻️ Reutilización total de filtros entre contextos
- 🔌 Independencia total del ORM
- 🧪 Fácil de testear — el Criteria es un objeto de dominio puro
- 🚀 Escalable a cualquier complejidad: joins, agregaciones, multi-tenant
💬 Conclusión
El patrón Criteria no es solo una mejora técnica: es un cambio de mentalidad.
Dejas de escribir queries específicas para cada caso y empiezas a construir un sistema flexible que evoluciona contigo. Y lo mejor: una vez lo implementas bien, no quieres volver atrás.
Publicado originalmente en LinkedIn.