¿Use case, o service con métodos?
El use case es la unidad por defecto: un fichero, una clase, un execute(). En petid-api hay
84. Services en application/ hay dos, y ambos llevan escrito en el código por qué existen y
cuándo dejarían de existir.
Use case por defecto. Service solo en dos situaciones concretas, abajo.
Excepción 1 — Un paso compartido, que nadie “pide”
PetAccessService. Toda lectura o escritura por debajo de una mascota hace lo mismo: cargarla,
404 si no está, y dejar que la entidad decida si el solicitante puede tocarla. Esa secuencia estaba
copiada en unos 15 use cases, “lo que significaba que un cambio en la regla había que acordarse de
hacerlo en 15 sitios”.
async assertPetAccessible(petId: string, requester: User): Promise<Pet> {
const pet = await this.pets.findById(petId);
if (!pet) throw new PetNotFoundError(petId);
pet.assertAccessibleBy(requester);
return pet;
}
Lo que lo distingue de un use case es que nadie pide “comprobar acceso a una mascota”. No es una operación del sistema, es un prerrequisito de otras. Un use case responde a algo que alguien quiere hacer; esto no.
Detalle de diseño que conviene copiar: devuelve la entidad que cargó. Y assertReportAccessible
devuelve { report, pet } porque tuvo que cargar los dos de todas formas — así quien necesita el
nombre de la mascota no vuelve a pedirla.
Excepción 2 — Muchas consultas sin lógica contra un read model
StatisticsQueryService. Cada gráfica tenía su use case de 18 líneas “de las que 17 eran ceremonia
de inyección”: 17 use cases que solo hacían return this.stats.getX(tenantId).
Se colapsan porque StatisticsRepository no es un repositorio de agregados: devuelve conteos
(MonthlyCount, TenantSummary), no entidades. Es un read model, así que ir directo no se salta
ninguna regla de negocio — no hay ninguna. La autorización la resuelven el guard y el tenantId del
usuario autenticado, antes de llegar.
El comentario del propio fichero incluye la condición de vuelta atrás, y esa parte es la importante:
Si alguna estadística gana lógica real —autorización cruzada, combinar fuentes, cachear— esa vuelve a tener su use case propio.
Y ya hay dos que nunca entraron: get-own-notification-stats y get-tenant-notification-stats
siguen siendo use cases, porque hablan con el gateway de notification-api, no con el read model.
Lo que NO justifica un service
Evitar duplicar la inyección. Si dos use cases inyectan el mismo repositorio, eso no es duplicación, es que los dos lo necesitan.
Agrupar operaciones de la misma entidad. Un PetsService con create, update, delete y
list es exactamente lo que el patrón evita: cuatro razones distintas para cambiar el mismo
fichero.
Reutilizar lógica entre use cases. Para eso, un use case inyecta a otro, que es lo normal aquí:
// CreatePetUseCase crea también la chapa
constructor(
@Inject(PET_REPOSITORY) private readonly pets: PetRepository,
private readonly createPetTagUC: CreatePetTagUseCase,
) {}
Fíjate en que el use case inyectado se pide por su clase, sin @Inject ni símbolo — no es una
interfaz, es una implementación concreta que vive en la misma capa.
Resumen para decidir
| Tienes… | Escribe |
|---|---|
| Una operación que alguien pide | Use case |
| Un prerrequisito que repiten muchos use cases | Service |
| N consultas sin reglas contra un read model | Service |
| Lógica compartida entre dos operaciones | Use case que inyecta al otro |
| Ganas de agrupar el CRUD de una entidad | Use case, uno por operación |