PHPackages                             tetthys/safejob - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. tetthys/safejob

ActiveLibrary[Queues &amp; Workers](/categories/queues)

tetthys/safejob
===============

Financial-grade idempotent job framework with Laravel integration

0.0.1(6mo ago)11MITPHPPHP ^8.5

Since Dec 30Pushed 6mo agoCompare

[ Source](https://github.com/tetthys/safe-job)[ Packagist](https://packagist.org/packages/tetthys/safejob)[ RSS](/packages/tetthys-safejob/feed)WikiDiscussions dev Synced today

READMEChangelog (1)Dependencies (5)Versions (2)Used By (0)

Tetthys SafeJob
===============

[](#tetthys-safejob)

**Tetthys SafeJob** is a financial-grade job execution framework that guarantees
**exactly-once success semantics** even under retries, duplicate dispatches, or worker crashes.

It is designed for high-risk domains such as payments, wallets, settlements, and external side effects, and integrates naturally with **Laravel 12 Queue** while remaining framework-agnostic at the core.

---

Why SafeJob?
------------

[](#why-safejob)

Laravel jobs are *at-least-once* by default.

This means:

- A job **may run more than once**
- `handle()` may be re-executed after partial success
- Side effects (payments, events, notifications) can be duplicated

SafeJob solves this by **splitting execution into two strictly separated phases**:

1. **Retry-safe work** (can run many times)
2. **Exactly-once success effects** (runs globally only once)

---

Execution Pipeline
------------------

[](#execution-pipeline)

Every SafeJob follows the same fixed pipeline:

```

┌────────────────────┐
│ tryLease()         │  Acquire global execution right
│ (DB row + TTL)     │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ perform()          │  Retry-safe, idempotent work
│ (may run many)     │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ finalizeSuccess()  │  Atomically mark SUCCEEDED
│ (exactly once)     │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ onSucceededOnce()  │  Irreversible side effects
│ (exactly once)     │
└────────────────────┘

```

If an exception occurs:

- If allowed by `FailurePolicy` → finalize as **FAILED**
- Otherwise → exception is rethrown and normal Laravel retry applies

---

Installation
------------

[](#installation)

```
composer require tetthys/safejob
php artisan migrate
```

This will:

- Register the Laravel service provider automatically
- Install the `safe_jobs` table for lease &amp; state tracking

---

Core Concepts
-------------

[](#core-concepts)

### SafeJob (Business Logic)

[](#safejob-business-logic)

You implement **SafeJob**, not Laravel Job logic.

```
use Tetthys\SafeJob\Core\Contracts\SafeJob;
use Tetthys\SafeJob\Core\Contracts\Context;
use Tetthys\SafeJob\Core\Contracts\IdempotencyKey;
use Tetthys\SafeJob\Core\Value\Outcome;

final class ChargeWalletSafeJob implements SafeJob
{
    public function __construct(
        private string $chargeId
    ) {}

    public function key(): IdempotencyKey
    {
        return new IdempotencyKey('charge:' . $this->chargeId);
    }

    public function perform(Context $ctx): Outcome
    {
        // Retry-safe work only:
        // - idempotent DB writes
        // - upserts
        // - unique constraints
        return Outcome::ranSuccess();
    }

    public function onSucceededOnce(Context $ctx): void
    {
        // Exactly-once side effects:
        // - emit domain event
        // - send notification
        // - publish message
    }
}
```

Rules:

- `perform()` **must be safe to run multiple times**
- `onSucceededOnce()` **must assume it runs only once globally**

---

Laravel Job Integration
-----------------------

[](#laravel-job-integration)

Your Laravel Job becomes a thin adapter.

```
use Illuminate\Contracts\Queue\ShouldQueue;
use Tetthys\SafeJob\Integration\Laravel\Concerns\HandlesSafeJob;
use Tetthys\SafeJob\Core\Contracts\SafeJob as SafeJobContract;

final class ChargeWalletJob implements ShouldQueue
{
    use HandlesSafeJob;

    public function __construct(
        public string $chargeId
    ) {}

    protected function safeJob(): SafeJobContract
    {
        return new ChargeWalletSafeJob($this->chargeId);
    }
}
```

That is all.

- No manual locking
- No duplicate guards
- No custom retry logic

---

Failure Handling (Optional)
---------------------------

[](#failure-handling-optional)

By default, **no failure is finalized**. All exceptions bubble up and Laravel retries normally.

You may define a `FailurePolicy` to explicitly finalize certain errors:

```
protected function failurePolicy(): FailurePolicy
{
    return new class implements FailurePolicy {
        public function finalFailureFor(\Throwable $e): ?FinalFailure
        {
            if ($e instanceof BusinessRuleViolation) {
                return new FinalFailure(
                    code: 'rule_violation',
                    message: $e->getMessage()
                );
            }
            return null;
        }
    };
}
```

Only when `FinalFailure` is returned will the job be permanently marked as **FAILED**.

---

Guarantees
----------

[](#guarantees)

SafeJob provides:

- ✅ Global **exactly-once success**
- ✅ Safe retries under crashes
- ✅ Duplicate dispatch protection
- ✅ Lease-based concurrency control
- ✅ Deterministic final state (SUCCEEDED / FAILED)

What it does **not** do:

- ❌ Distributed transactions
- ❌ Automatic idempotency inside your domain logic

---

When to Use
-----------

[](#when-to-use)

SafeJob is ideal for:

- Payments / Wallets / Escrow
- External API side effects
- Event publishing
- Financial or legal workflows
- Any job where “running twice” is unacceptable

---

Design Philosophy
-----------------

[](#design-philosophy)

> **Jobs must be retryable. Side effects must be final. The boundary must be explicit.**

SafeJob enforces this boundary by design.

---

License
-------

[](#license)

MIT

###  Health Score

31

—

LowBetter than 66% of packages

Maintenance68

Regular maintenance activity

Popularity3

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

185d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/115064626?v=4)[tetthys](/maintainers/tetthys)[@tetthys](https://github.com/tetthys)

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/tetthys-safejob/health.svg)

```
[![Health](https://phpackages.com/badges/tetthys-safejob/health.svg)](https://phpackages.com/packages/tetthys-safejob)
```

###  Alternatives

[illuminate/queue

The Illuminate Queue package.

21332.6M1.6k](/packages/illuminate-queue)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M194](/packages/laravel-ai)[mike-bronner/laravel-model-caching

Automatic caching for Eloquent models.

2.4k90.5k1](/packages/mike-bronner-laravel-model-caching)[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[spatie/laravel-health

Monitor the health of a Laravel application

87512.0M165](/packages/spatie-laravel-health)[api-platform/laravel

API Platform support for Laravel

58171.4k14](/packages/api-platform-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
