Una pregunta que merece la pena hacerse antes de añadir nada a un paquete compartido: ¿este comportamiento pertenece a todos los servicios, o pertenece a este servicio?
La creación de licencias pertenece a api-license. LicenseManager, LicenseFacade, LicenseDTO, el driver de Statamic que hay debajo: nada de eso vive en el paquete compartido, ni debería. Una licencia no es una preocupación que tengan todas las APIs de esta flota. Sólo una la tiene. Si LicenseManager migrara al paquete compartido «por consistencia», todos los demás servicios compilarían en su construcción una dependencia del modelo de licencias de Statamic sin razón alguna, y el día que api-license necesitara cambiar cómo se emite una licencia, ese cambio tendría que pasar por el mismo peaje de coordinación que un cambio en la autenticación, para una preocupación que sólo tiene un consumidor.
La prueba es simple, y merece la pena escribirla porque es la única pregunta que evita que un paquete compartido se convierta en un cajón de sastre: si quitaras este código del paquete compartido y lo copiaras y pegaras en cada consumidor, ¿acabarían siendo idénticas todas las copias? La autenticación pasa esa prueba. El límite de frecuencia la pasa. Un cliente HTTP con reintentos la pasa. La lógica de negocio específica de un dominio nunca la pasa, porque en cuanto la copias en un segundo consumidor empieza a desviarse para encajar en el problema real de ese consumidor, no en el compartido. Un paquete compartido guarda el código que se supone idéntico en todas partes. Lo que se supone que debe diferir por servicio no pertenece ahí, por muy «de infraestructura» que parezca.
El paquete que no debe convertirse en servicio
¿Por qué llamarlo «paquete compartido» en lugar de «servicio compartido»? Porque en cuanto empieza a comportarse como un servicio —guardando su propio estado, escuchando en su propio puerto, poseyendo su propia cola— deja de ser algo que cada aplicación hereda calladamente y pasa a ser una dependencia sin documentar que nadie puso en el diagrama de arquitectura.
Mira cómo funciona realmente el registro de peticiones. LogApiRequestsMiddleware viaja con el paquete y corre dentro del proceso de la aplicación que atendió la petición. Fíjate en lo que ese middleware no hace: no abre un socket a un servidor de registros, no acumula nada en su propia memoria. Sanea la petición y la respuesta y luego difiere un job hacia la conexión de cola que la aplicación anfitriona ya tiene configurada. El propio job resuelve su nombre de cola desde la configuración en lugar de fijarlo a fuego:
// vendor/webplo/api-infrastructure/src/Jobs/SendToLogsJob.php
public function __construct(
public readonly int $user_id,
public readonly string $method,
public readonly string $endpoint,
// ...remaining constructor promoted properties
) {
$this->onQueue(config('webplo.queue', 'default'));
}
public function handle(): void
{
LogFacade::create(/* ...same parameters, forwarded */);
}
SendToLogsJob corre en el worker de la propia aplicación anfitriona, el mismo proceso que ejecuta todos los demás jobs de esa aplicación. El paquete no levantó infraestructura para ejecutarlo: tomó prestada la que ya estaba ahí.
Esa es la lección dos: una librería compartida debe seguir siendo una librería. Su código se ejecuta sobre infraestructura prestada —el proceso del anfitrión, su conexión de cola, su almacén de caché configurado— nunca sobre infraestructura que el paquete levantó para sí mismo. En cuanto un «paquete compartido» empieza a ejecutar su propio worker, a mantener su propia conexión de base de datos o a exponer su propio endpoint al que todos los servicios llaman calladamente, se ha convertido en un servicio en todos los sentidos que importan salvo en el que aparece en tu diagrama de arquitectura o en tu turno de guardia. Ese es el peor tipo de servicio que se puede tener: el invisible, del que nadie recuerda que depende que esté en pie, hasta que no lo está.