ecosystem-wikicómo extender los templates sin romper sus convenciones
‹ todas las recipes

¿Use case, o service con métodos?

template-api
último cambio 2026-09-25

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