La Arquitectura Hexagonal
Contratos, Interfaces, Repositorios, Servicios y Value Objects
Este documento explica los conceptos fundamentales de Arquitectura Hexagonal (Ports & Adapters) aplicados al proyecto ControlEscolar, una aplicación personal para que un alumno de la UnADM gestione sus materias, inscripciones y calificaciones utilizando buenas prácticas de ingeniería de software modernas (DDD + .NET 10 LTS).
El objetivo es enseñar, de manera clara y práctica, cómo separar la lógica del dominio de los detalles técnicos como bases de datos, ORMs, APIs o interfaces de usuario.
1. ¿Qué es un Value Object?
En Domain-Driven Design un Value Object (VO) es un tipo del dominio cuyo valor está determinado únicamente por sus atributos, no por identidad.
Ejemplos típicos en software:
- Dinero (cantidad + moneda)
- Coordenada (latitud + longitud)
- Intervalo de fechas
En ControlEscolar son Value Objects:
CodigoMateriaPeriodoLectivo- (Opcionalmente)
Calificacion
Propiedades de un Value Object
-
Inmutables
Una vez creados no se modifican. -
Siempre válidos
El constructor garantiza que el estado interno es correcto. -
Igualdad estructural
Dos VOs son iguales si sus valores son iguales. -
No tienen identidad
No necesitan ID ni se rastrean como entidades propias.
¿Por qué usarlos?
- Evitan duplicar validaciones.
- Aumentan la claridad del dominio.
- Previenen estados inconsistentes.
- Facilitan las pruebas unitarias.
En ControlEscolar, CodigoMateria valida y expresa la estructura oficial de una materia; PeriodoLectivo asegura que un periodo siempre tenga un año válido y una parte 1 o 2.
2. ¿Qué son los Contratos en Arquitectura Hexagonal?
En Arquitectura Hexagonal, un Contrato es una interfaz definida dentro del Dominio que describe qué necesita el dominio del mundo exterior, sin conocer detalles técnicos.
El dominio NO debe depender de:
- EF Core
- SQL Server
- APIs externas
- Archivos
- Frameworks
Por eso define interfaces, no implementaciones.
Un Contrato es entonces:
Una promesa sobre el comportamiento esperado, sin definir cómo se llevará a cabo.
Ejemplo de contrato en ControlEscolar:
public interface IMateriaRepository {
Task<Materia?> ObtenerPorIdAsync(Guid id);
Task<Materia?> ObtenerPorCodigoAsync(CodigoMateria codigo);
Task<List<Materia>> ListarTodasAsync();
}
El dominio dice “necesito estas operaciones”, pero no sabe cómo se implementan.
3. Interfaces (Ports)
Una interfaz en el dominio es la expresión concreta del contrato.
En Hexagonal, estas interfaces son los puertos por los que el dominio se comunica con la infraestructura.
Características
- Se definen en el Dominio.
- No contienen código técnico.
- No conocen SQL, EF Core ni HTTP.
- Son consumidas por los casos de uso (Application layer).
¿Por qué son necesarias?
Porque permiten que:
- El dominio sea agnóstico a la tecnología.
- La aplicación pueda tener múltiples implementaciones (SQL Server, pruebas en memoria, mocks).
- El sistema sea testeable sin levantar una base de datos.
- Puedas cambiar de tecnología sin reescribir el dominio.
Sin interfaces, EF Core gobernaría la arquitectura y el dominio quedaría contaminado.
4. Repositorios: Puertos para acceder a datos del dominio
Un Repositorio es un tipo especial de contrato cuyo propósito es proporcionar acceso a entidades del dominio como si fueran colecciones en memoria.
Los repositorios pertenecen al Dominio, NO a la infraestructura.
Ejemplos en ControlEscolar:
- Acceder a materias del catálogo
- Leer o guardar inscripciones
- Obtener actividades de una inscripción
Ejemplo:
public interface IInscripcionRepository {
Task<Inscripcion?> ObtenerInscripcionActivaAsync(MateriaId materiaId, PeriodoLectivo periodo);
Task<List<Inscripcion>> ObtenerHistorialAsync(MateriaId materiaId);
Task GuardarAsync(Inscripcion inscripcion);
}
La implementación técnica vive en Infrastructure.
5. Servicios en la Arquitectura Hexagonal
Existen dos tipos:
5.1 Servicios de Dominio
Son clases que encapsulan reglas de negocio puras, que no pertenecen naturalmente a una sola entidad o value object.
- No conocen repositorios.
- No acceden a infraestructura.
- Solo contienen lógica del dominio.
Ejemplo (simplificado):
public static class InscripcionRules {
public static bool PuedeInscribirse(Materia materia, Historial historial) {
return historial.Intentos < 3 && historial.PrerrequisitoAprobado;
}
}
5.2 Servicios de Aplicación
Son los orquestadores de casos de uso.
Se encuentran fuera del dominio y SÍ utilizan repositorios.
Ejemplo:
public class InscribirMateriaHandler {
private readonly IMateriaRepository _materias;
private readonly IInscripcionRepository _inscripciones;
public async Task Ejecutar(InscribirMateriaCommand cmd) {
var materia = await _materias.ObtenerPorCodigoAsync(cmd.Codigo);
var historial = await _inscripciones.ObtenerHistorialAsync(materia.Id);
// reglas de negocio (dominio)
if (!InscripcionRules.PuedeInscribirse(materia, historial))
throw new Exception("No puedes inscribirte.");
var insc = new Inscripcion(materia.Id, cmd.Periodo, cmd.Asesor);
await _inscripciones.GuardarAsync(insc);
}
}
6. Adaptadores (Adapters): Implementaciones técnicas
La infraestructura implementa los contratos definidos en el dominio usando la tecnología elegida.
Ejemplo con SQL Server + EF Core:
public class SqlMateriaRepository : IMateriaRepository {
private readonly AppDbContext _db;
public SqlMateriaRepository(AppDbContext db) {
_db = db;
}
public Task<List<Materia>> ListarTodasAsync() =>
_db.Materias.ToListAsync();
}
El dominio nunca ve esta clase; solo ve la interfaz.
7. Contratos definidos para ControlEscolar
Basado en el dominio v1.0, los contratos iniciales recomendados son:
IMateriaRepository
public interface IMateriaRepository {
Task<Materia?> ObtenerPorIdAsync(Guid id);
Task<Materia?> ObtenerPorCodigoAsync(CodigoMateria codigo);
Task<List<Materia>> ListarTodasAsync();
}
IInscripcionRepository
public interface IInscripcionRepository {
Task<Inscripcion?> ObtenerInscripcionActivaAsync(Guid materiaId, PeriodoLectivo periodo);
Task<List<Inscripcion>> ObtenerHistorialAsync(Guid materiaId);
Task GuardarAsync(Inscripcion inscripcion);
}
IActividadRepository (dependiendo si decides separar actividades)
public interface IActividadRepository {
Task<List<Actividad>> ObtenerPorInscripcionAsync(Guid inscripcionId);
Task GuardarAsync(Actividad actividad);
}
IPeriodoLectivoProvider (opcional)
Para obtener el periodo actual cuando no se pasa explícitamente:
public interface IPeriodoLectivoProvider {
PeriodoLectivo ObtenerPeriodoActual();
}
8. Resumen visual de Hexagonal Architecture
+------------------+
| Dominio |
| Entidades, VO, |
| Servicios, |
| Interfaces |
+--------+---------+
^
|
(Ports/Interfaces)
|
+---------+----------+
| Aplicación |
| Casos de uso |
+---------+----------+
^
|
Implementaciones
(Adapters)
|
+-----------+--------------+
| Infraestructura |
| SQL Server, EF, Archivos |
+---------------------------+
9. Conclusiones
En ControlEscolar, la Arquitectura Hexagonal permite:
- Diseñar reglas académicas coherentes sin contaminar el dominio con tecnología.
- Cambiar fácilmente de SQL Server a PostgreSQL o a un repositorio en memoria.
- Probar la lógica sin base de datos.
- Mantener el sistema limpio, modular y extensible.
Los contratos (interfaces), los Value Objects, los Repositorios y los Servicios son los pilares que permiten esta separación estricta.
Con el dominio ya definido y los contratos establecidos, el siguiente paso es comenzar a escribir la capa de Domain en .NET 10 LTS.