Skip to content

ADR-004: Wrapper Pattern (NO Refactoring)

Fecha: 2026-02-05 Estado: Aprobado Deciders: Architecture Team, Backend Team

Contexto y Problema

Queremos agregar ejecución asíncrona a operaciones existentes que funcionan sincrónicamente. Por ejemplo:

  • FacturaService::batchInvoice($data) existe y funciona bien
  • Queremos que TAMBIÉN se pueda ejecutar en background (async)
  • NO queremos refactorizar FacturaService (riesgo de bugs, testing masivo)

Pregunta: ¿Cómo agregar async sin modificar código existente?

Opciones Consideradas

Opción A: Wrapper Pattern (SELECCIONADA)

Descripción:

  • Handler implementa JobHandlerInterface
  • Handler recibe el service existente como dependencia
  • Handler reconstruye el request DTO que espera el service
  • Handler delega TODA la lógica al service existente
  • Service NO sabe que está siendo ejecutado en background

Código:

php
class BatchInvoicingJobHandler implements JobHandlerInterface
{
    public function __construct(
        private FacturaService $service // Servicio existente SIN MODIFICAR
    ) {}

    public function getType(): string
    {
        return 'batch_invoicing';
    }

    public function handle(array $payload): array
    {
        // 1. Reconstruir DTO que espera el service
        $dto = new BatchInvoiceRequest(
            cliente_ids: $payload['cliente_ids'],
            fecha: $payload['fecha'],
            // ...
        );

        // 2. Delegar al service (TODA la lógica está ahí)
        $result = $this->service->batchInvoice($dto);

        // 3. Retornar resultado
        return $result;
    }
}

Pros:

  • ✅ CERO modificaciones al service existente
  • ✅ Service puede usarse sincrónicamente (controller) o asincrónicamente (handler)
  • ✅ Rollback instantáneo (feature flag OFF = usa service directo)
  • ✅ Testing simple (handler unit test mockea service)
  • ✅ Bajo riesgo (NO tocamos código que funciona)

Contras:

  • ❌ Duplicación leve (handler + controller construyen DTO similar)
  • ❌ Handler debe conocer interface del service

Opción B: Refactorizar Service para Soportar Ambos Modos

Descripción:

  • Service tiene flag $async en constructor o método
  • Si $async=true, service crea job en BD y retorna job_id
  • Si $async=false, service ejecuta sincrónicamente (legacy)

Código:

php
class FacturaService
{
    public function batchInvoice(BatchInvoiceRequest $data, bool $async = false): mixed
    {
        if ($async) {
            // Crear job y retornar job_id
            $jobId = $this->jobDispatcher->dispatch('batch_invoicing', $data);
            return ['job_id' => $jobId];
        }

        // Ejecución síncrona (legacy)
        return $this->executeBatchInvoice($data);
    }
}

Pros:

  • ✅ Una sola interface (controller siempre llama al service)
  • ✅ NO duplicación (lógica en un solo lugar)

Contras:

  • ❌ Service ahora depende de background jobs (acoplamiento)
  • ❌ Testing más complejo (mockear JobDispatcher)
  • ❌ Refactoring de servicio existente (riesgo de bugs)
  • ❌ Viola Single Responsibility (service hace dispatch Y ejecución)

Veredicto: ❌ Descartado (acoplamiento, refactoring riesgoso)


Opción C: Service Delega a Handler

Descripción:

  • Service recibe JobHandlerInterface como dependencia
  • Service delega ejecución al handler (inversión)

Código:

php
class FacturaService
{
    public function __construct(
        private JobHandlerInterface $handler
    ) {}

    public function batchInvoice(BatchInvoiceRequest $data): array
    {
        return $this->handler->handle($data->toArray());
    }
}

Pros:

  • ✅ Service es thin wrapper (lógica en handler)

Contras:

  • ❌ Inversión de dependencias (service depende de handler)
  • ❌ Handler contendría lógica de negocio (violaría patrón)
  • ❌ Service pierde razón de ser (se vuelve proxy)

Veredicto: ❌ Descartado (inversión incorrecta, viola arquitectura)


Decisión

Seleccionamos Opción A: Wrapper Pattern

Justificación:

  • Patrón estándar para agregar features incrementales sin modificar código existente
  • Bajo riesgo (código que funciona NO se toca)
  • Feature flag permite rollback instantáneo
  • Consistente con arquitectura existente (handlers son Strategy Pattern)

Consecuencias

Positivas

  • ✅ CERO impacto en código legacy
  • ✅ Feature flag permite rollout gradual por módulo
  • ✅ Fácil testing (unit test handler con mock service)
  • ✅ Service puede usarse en ambos contextos (sync/async)

Negativas

  • ❌ Leve duplicación (handler + controller construyen DTO)
  • ❌ Si service cambia interface, handler debe actualizarse

Mitigaciones

Mitigaciones:

  1. Duplicación: Mínima, solo construcción de DTO (5-10 líneas)
  2. Interface changes: Tests fallan si handler desincronizado (protección)

Implementación

Controller con feature flag:

php
public function batchInvoice(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
    $payload = $request->getParsedBody();

    if ($this->config['background_jobs']['enabled']) {
        // Flujo asíncrono (nuevo)
        $jobId = $this->jobDispatcher->dispatch(
            'batch_invoicing',
            $payload,
            $this->auth->getUserId(),
            $request->getHeaderLine('X-Schema')
        );

        return $this->jsonResponse($response, [
            'status' => 'accepted',
            'job_id' => $jobId
        ], 202);

    } else {
        // Flujo síncrono (legacy - sin cambios)
        $dto = new BatchInvoiceRequest($payload);
        $result = $this->service->batchInvoice($dto);

        return $this->jsonResponse($response, $result, 200);
    }
}

Handler (wrapper):

php
class BatchInvoicingJobHandler implements JobHandlerInterface
{
    public function __construct(
        private FacturaService $service // Inyectado, NO modificado
    ) {}

    public function handle(array $payload): array
    {
        // Reconstruir DTO que espera el service
        $dto = new BatchInvoiceRequest($payload);

        // Delegar TODA la lógica
        return $this->service->batchInvoice($dto);
    }
}

Referencias