¿Repositorio o port?
Las dos cosas son interfaces que alguien implementa fuera. Se parecen tanto que es fácil poner una donde va la otra, y viven en carpetas distintas a propósito.
| Repositorio | Port | |
|---|---|---|
| Dónde vive | domain/<agregado>/x.repository.ts |
application/ports/x.port.ts |
| Qué devuelve | Entidades de dominio | Primitivas, objetos planos, o nada |
| Qué modela | Cómo se guarda algo tuyo | Una capacidad técnica o un sistema ajeno |
| Cuántos hay | 15 en petid-api |
15 en petid-api |
La regla
Mira la firma. Si menciona un tipo que no es entidad tuya, es un port.
// Repositorio: devuelve PetTag, que es tuyo → domain/
findByPublicCode(publicCode: string): Promise<PetTag | null>;
// Port: devuelve un Readable de Node, que no es tuyo → application/ports/
download(key: string): Promise<Readable>;
El caso que más confunde es un gateway que devuelve datos, como el de logs de notificaciones.
Parece un repositorio: tiene consultas, devuelve filas, hasta pagina. Pero lo que devuelve es una
interface NotificationLog declarada en el propio fichero del port — un objeto plano, no una
entidad con comportamiento. Esos datos son de otro servicio; tú solo los lees de paso.
Las tres familias de port
Los 15 de petid-api caen aquí:
- Capacidades técnicas:
Clock,PasswordHasher,TokenHasher,Jwt,Totp,SecretCipher. Lo que envuelven es una librería o el propio sistema. - Recursos externos:
FileStorage,PdfGenerator,EmailSender,PushSender. - Gateways a otro servicio (5, todos a notification-api): logs, plantillas, configuración de proveedor, tenants y creación de tenant.
Clock merece un comentario aparte: envolver new Date() en un port parece exagerado hasta que
tienes que testear un enfriamiento de 10 minutos. Con el port, el test pasa la hora que quiera.
Lo que hace que el patrón valga la pena
Cada port tiene dos implementaciones: la real y una degradada.
infrastructure/storage/ minio-file-storage.ts + unavailable-file-storage.ts
infrastructure/push/ notification-service-* + console-push-sender.ts
infrastructure/email/ email-service-sender.ts + console-email-sender.ts
infrastructure/notifications/ notification-service-*-gateway.ts + unavailable-*-gateway.ts
El módulo elige según configuración. Por eso la api arranca sin MinIO y sin notification-api: no revienta en el arranque, falla en el primer uso real y con un mensaje claro. Si añades un port, añade también su versión degradada — es la mitad del valor.
Cómo se escribe un port
Interfaz y Symbol en el mismo fichero, igual que un repositorio:
// application/ports/file-storage.port.ts
export interface UploadInput {
key: string;
body: Buffer;
mimeType: string;
}
export interface FileStorage {
upload(input: UploadInput): Promise<void>;
delete(key: string): Promise<void>;
download(key: string): Promise<Readable>;
}
export const FILE_STORAGE = Symbol("FileStorage");
Los tipos auxiliares que necesite (UploadInput) van en el propio fichero del port, no en
domain/. No son del negocio, son de la frontera.
Trampas
- No metas un port en
domain/aunque lo use una entidad. El dominio no importa nada, y eso incluye ports. - No devuelvas entidades desde un port. Si lo estás haciendo, probablemente es un repositorio mal colocado.
- No crees un port para algo que no vas a sustituir ni simular.
randomUUID()se llama directo en los use cases; nadie lo ha envuelto, y está bien así.