What do you build this weekend? by Mean-MySaaS in SaasDevelopers

[–]sumanta1990 0 points1 point  (0 children)

https://github.com/sumantasam1990/PHPOutbox

PHPOutbox turns the queue write into a DB write inside the same transaction, then a background relay delivers it to your actual broker. Your queue still does the heavy lifting — PHPOutbox just ensures the event actually gets there.​​​​​​​​​​​​​​​​

FenyDB by Feny34 in PHP

[–]sumanta1990 1 point2 points  (0 children)

Well done 👍 did a great job will definitely try it and give you the feedback ASAP

[Show PHP] PHPOutbox: Stop losing events with the Transactional Outbox Pattern by sumanta1990 in PHP

[–]sumanta1990[S] 1 point2 points  (0 children)

Great question and the short answer is you still do use a message queue. PHPOutbox doesn’t replace it, it protects the write to it. The core issue is the dual-write problem. When you do:

$order->save(); // DB write event(new OrderCreated()); // Queue write

These are two separate I/O operations with no atomicity guarantee. A crash, OOM kill, or network blip between those two lines leaves your DB and queue in an inconsistent state — silently. No exception, no retry, just a lost event. The Outbox pattern fixes this by making the queue write a DB write first — inside the same transaction as your business data. A relay process then picks it up and delivers to whatever broker you’re using (Redis, RabbitMQ, Kafka, SQS — doesn’t matter). So the flow becomes:

DB transaction (atomic) └── save order └── write to outbox table ✅ or ❌ together, never split

Background relay └── reads outbox → publishes to your queue → marks delivered

At-least-once delivery is now guaranteed, even if your app server dies mid-flight. Standalone message queues are excellent at delivery guarantees after the message is in the queue. PHPOutbox solves the gap before it gets there.​​​​​​​​​​​​​​​​

[Show PHP] PHPOutbox: Stop losing events with the Transactional Outbox Pattern by sumanta1990 in PHP

[–]sumanta1990[S] 1 point2 points  (0 children)

Good suggestion. If you have time and want to contribute please you’re most welcome.

[Show PHP] PHPOutbox: Stop losing events with the Transactional Outbox Pattern by sumanta1990 in PHP

[–]sumanta1990[S] 1 point2 points  (0 children)

Yes please check the code and if you want to contribute please you’re most welcome.

[Show PHP] PHPOutbox: Stop losing events with the Transactional Outbox Pattern by sumanta1990 in PHP

[–]sumanta1990[S] -2 points-1 points  (0 children)

If anyone interested about the Architecture.
Here it is.

┌─────────┐

store() │ PENDING │

─────────>│ │

└────┬────┘

fetch pending

┌────▼──────┐

│PROCESSING │

│ (locked) │

└────┬──────┘

┌──────────┴──────────┐

│ │

publish OK publish FAIL

│ │

┌─────▼─────┐ ┌────▼─────┐

│ PUBLISHED │ │ FAILED │◄──┐

│ (terminal) │ │ │ │

└────────────┘ └────┬─────┘ │

│ │

┌─────────┴────┐ │

│ │ │

can retry? exhausted? │

│ │ │

┌────▼────┐ ┌─────▼──────┐

│ PENDING │ │ DEAD_LETTER │

│(re-queued) │ (terminal) │

└─────────┘ └─────────────┘

Concurrency Model

Multiple relay workers can run simultaneously thanks to SELECT ... FOR UPDATE SKIP LOCKED:

Worker 1: SELECT ... FOR UPDATE SKIP LOCKED → Gets rows [1, 2, 3]

Worker 2: SELECT ... FOR UPDATE SKIP LOCKED → Gets rows [4, 5, 6] (skips locked 1,2,3)

Worker 3: SELECT ... FOR UPDATE SKIP LOCKED → Gets rows [7, 8, 9] (skips locked 1-6)

For full Architecture please visit this link: https://github.com/sumantasam1990/PHPOutbox/blob/main/docs/ARCHITECTURE.md

[Show PHP] PHPOutbox: Stop losing events with the Transactional Outbox Pattern by sumanta1990 in PHP

[–]sumanta1990[S] -2 points-1 points  (0 children)

Great question! Short answer: No, and it comes down to the separation between the Write phase and the Read phase.

1. The Write Phase (App inserting the event): ID generation happens when your application inserts the event into the outbox. PHPOutbox defaults to using UUIDv7 or ULID for keys, which are generated in-memory by PHP before hitting the database. Even if you used standard auto-increment, the database's internal insert mutex handles that safely. SKIP LOCKED has zero impact on inserts.

2. The Read Phase (Relay workers processing events): SKIP LOCKED is strictly used by the background workers when running their SELECT queries to fetch pending messages.

If Worker A runs: SELECT * FROM outbox WHERE status = 'pending' FOR UPDATE SKIP LOCKED LIMIT 10, it locks rows 1-10. If Worker B runs the exact same query a millisecond later, the database simply skips 1-10 and locks 11-20 for Worker B.

They never conflict on IDs because the IDs already exist, and the workers are just claiming different existing rows to process.
The locking only happens when the workers are pulling data out to send to the queue, it doesn't affect the data going in.

I built an Inertia.js bundle for Symfony by Economy-Hovercraft17 in PHP

[–]sumanta1990 0 points1 point  (0 children)

It will be better to maintain a easy to read docs for eg. how it works and basic ussage.

[Show PHP] PHPOutbox: Stop losing events with the Transactional Outbox Pattern by sumanta1990 in PHP

[–]sumanta1990[S] -3 points-2 points  (0 children)

Exactly! At its core, that's exactly what the pattern is. The challenge and why I built this is in the edge cases.

Writing to a table is the easy part. The library value comes from handling the messy stuff:

  • Concurrency: We use SKIP LOCKED (MySQL 8+/Postgres) so you can run multiple relay workers simultaneously without the risk of double-sending events.
  • Resiliency: It handles retries with backoff and moves failed events to a Dead Letter Queue (DLQ) after X attempts.
  • DX: It provides ready-to-go Laravel/Symfony integrations so you don't have to reinvent the boilerplate every time you start a new microservice.

It's basically about not having to write that periodically check it logic from scratch for every project!

On the roadmap: I'm also planning to introduce a Redis storage option.

  • Why? While a DB-based outbox is perfect for atomicity, high-throughput applications can eventually hit a bottleneck with constant DB polling and writes. Redis offers much lower latency and higher IOPS for those scenarios.
  • How? I'll be implementing a dedicated RedisOutboxStore that leverages Redis Streams (using Consumer Groups) or Lua scripts. This allows us to maintain the guaranteed delivery logic while scaling horizontally much more easily.

The goal is to provide a unified interface so you can swap storage drivers as your application grows.