Skip to content

Job

Job is an idempotent queued inbound adapter that re-establishes context before one UseCase.

Generated facts

Layer
service
Generated path
app/Pulsar/Services/{Service}/Modules/{Module}/Jobs/{Name}.php
Generator command
make:job
Workflow method
None
Stability
Current manifest surface (Pulsar 0.4.1)

Canonical example

Generated src/stubs/job.stub — canonical synchronized stub.

<?php

namespace {{namespace}};

use {{useCaseNamespace}}\{{useCase}};
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Gate;
use Throwable;

class {{name}} implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;

    public int $timeout = 120;

    public function __construct(
        public readonly string $actorId,
        public readonly string $tenantId,
        public readonly string $idempotencyKey,
    ) {
        // Carry only IDs, scalars, DTOs, or Value Objects. Never Eloquent models.
        // Do not add SerializesModels: a worker must receive explicit identity or snapshot data.
    }

    /**
     * @return list<int>
     */
    public function backoff(): array
    {
        return [10, 30, 60];
    }

    public function handle({{useCase}} $useCase): void
    {
        // Re-establish actor and tenant context from the carried identifiers before authorizing.
        // Replace this framework string with the owning domain's backed Ability enum value.
        Gate::authorize('replace-with-domain-ability', $this->actorId);

        // Queues are at-least-once: make this workflow idempotent on $idempotencyKey.
        // The UseCase owns the transaction; this Job must contain no branching business logic.
        $useCase->execute($this->tenantId, $this->idempotencyKey);
    }

    public function failed(Throwable $exception): void
    {
        // Report terminal failure without starting a replacement workflow here.
    }
}

Responsibility

Job owns the responsibility stated above; it must not absorb delivery, transaction, or unrelated cross-layer behavior.

Placement and dependencies

Keep this type in its generated Service Module path. It may depend inward on Domain capability, but it must not make another Service its behavioral dependency.

Workflow and tests

Job is an inbound adapter: establish the delivery boundary, then call one UseCase. Test its direct contract, its rejected boundary cases, and the caller or callee that proves the rule in application flow.

Three pitfalls

  1. ❌ Prohibited: move this responsibility into a neighboring type merely because it is nearby.
  2. ✅ Correct: keep the generated placement and depend only on the documented layer direction.
  3. ✅ Correct: test the boundary through the caller and the return or side effect visible to its callee.

Related reading

Read architecture placement for shared rationale and follow this page’s related concept links for the adjacent responsibility.

Boundaries

❌ Prohibited: Do not serialize Eloquent models or place branching business logic in a Job.

✅ Correct: Carry IDs or value data and call one UseCase after authorization.