PHPackages                             tiny-blocks/outbox - 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. [Database &amp; ORM](/categories/database)
4. /
5. tiny-blocks/outbox

ActiveLibrary[Database &amp; ORM](/categories/database)

tiny-blocks/outbox
==================

Write-side adapter for the Transactional Outbox pattern that persists domain events atomically with aggregate state through Doctrine DBAL.

3.2.0(1mo ago)118MITPHP ^8.5

Since May 14Compare

[ Source](https://github.com/tiny-blocks/outbox)[ Packagist](https://packagist.org/packages/tiny-blocks/outbox)[ Docs](https://github.com/tiny-blocks/outbox)[ RSS](/packages/tiny-blocks-outbox/feed)WikiDiscussions Synced 3w ago

READMEChangelog (5)Dependencies (23)Versions (6)Used By (0)

Outbox
======

[](#outbox)

[![License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)](https://github.com/tiny-blocks/outbox/blob/main/LICENSE)

- [Overview](#overview)
- [Installation](#installation)
- [How to use](#how-to-use)
    - [Expected table schema](#expected-table-schema)
    - [Wiring the repository](#wiring-the-repository)
    - [Producing events from an aggregate](#producing-events-from-an-aggregate)
    - [Declaring integration events and translators](#declaring-integration-events-and-translators)
    - [Customizing the table layout](#customizing-the-table-layout)
    - [Writing a custom payload serializer](#writing-a-custom-payload-serializer)
    - [Event schema versioning](#event-schema-versioning)
- [FAQ](#faq)
- [License](#license)
- [Contributing](#contributing)

Overview
--------

[](#overview)

The **Transactional Outbox** pattern solves the dual-write problem: persisting an aggregate state change and publishing an integration event must happen atomically. Doing both independently risks a crash leaving one side committed and the other lost. The outbox pattern records both in the same database transaction, delegating event delivery to a separate relay process.

This library is the write-side adapter. It persists integration events atomically with aggregate state via Doctrine DBAL. The pipeline is: `DomainEvent → IntegrationEventTranslator → IntegrationEvent → PayloadSerializer → INSERT`. Domain events without a registered translator are silently skipped: their absence from `IntegrationEventTranslators`is the canonical declaration that the event is internal to the bounded context and must not cross its boundary.

The library is opinionated on correctness: transactions are always required, JSON validity is always checked, and every schema decision is left to you: table name, column names, and identity column storage type are all configurable.

The library composes with [`tiny-blocks/building-blocks`](https://github.com/tiny-blocks/building-blocks), which contributes `DomainEvent`, `EventRecord`, `EventRecords`, `IntegrationEvent`, `IntegrationEventBehavior`, `IntegrationEventRecord`, `IntegrationEventTranslator`, `IntegrationEventTranslators`, `EventType`, `Revision`, `AggregateVersion`, and the `EventualAggregateRoot` family. This library provides the persistence step only.

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

[](#installation)

```
composer require tiny-blocks/outbox

```

How to use
----------

[](#how-to-use)

### Expected table schema

[](#expected-table-schema)

The library does not create or manage the outbox table. Add it in your own migration.

**Default schema (BINARY(16) identity columns, recommended for UUID-based aggregates):**

```
CREATE TABLE outbox_events
(
    id                BINARY(16)   NOT NULL COMMENT 'The event identifier in Version 4 UUID format (e.g. 123e4567-e89b-12d3-a456-426614174000).',
    payload           JSON         NOT NULL COMMENT 'The event payload serialized as a JSON object (e.g. {"transaction_id":"..."}).',
    revision          INT          NOT NULL COMMENT 'The positive integer indicating the payload schema revision of the integration event (e.g. 1).',
    event_type        VARCHAR(255) NOT NULL COMMENT 'The integration event class name in CamelCase (e.g. PaymentConfirmed). Reflects the public contract, not the internal domain event.',
    occurred_at       TIMESTAMP(6) NOT NULL COMMENT 'The UTC date and time when the event occurred in ISO 8601 format (e.g. 2026-02-13T08:49:44.931408+00:00).',
    aggregate_id      BINARY(16)   NOT NULL COMMENT 'The aggregate root identifier in Version 4 UUID format (e.g. 123e4567-e89b-12d3-a456-426614174000).',
    aggregate_type    VARCHAR(255) NOT NULL COMMENT 'The aggregate root class name that produced the event in CamelCase (e.g. Transaction).',
    aggregate_version BIGINT       NOT NULL COMMENT 'The version of the aggregate at the moment the event was emitted, used to detect duplicate or out-of-order events per aggregate (e.g. 1).',
    created_at        TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'The UTC date and time when the record was inserted in ISO 8601 format (e.g. 2026-02-13T08:49:44.931408+00:00).',
    PRIMARY KEY (id),
    CONSTRAINT unq_outbox_events_aggregate_type_aggregate_id_aggregate_version UNIQUE (aggregate_type, aggregate_id, aggregate_version)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_0900_ai_ci COMMENT ='Table used to persist append-only outbox events for atomic event publication.';
```

The library writes to `id`, `aggregate_id`, `aggregate_type`, `event_type`, `revision`, `aggregate_version`, `payload`, and `occurred_at`. It never writes to `created_at`. The database fills it automatically.

For aggregates whose identities are not UUID v4 strings, use VARCHAR columns and configure `IdentityColumnType::STRING`(see [Customizing the table layout](#customizing-the-table-layout)):

```
CREATE TABLE outbox_events
(
    -- For non-UUID identities: VARCHAR(36) or wider.
    id           VARCHAR(36) NOT NULL,
    aggregate_id VARCHAR(36) NOT NULL
    -- All other columns are the same as the default schema.
)
```

### Wiring the repository

[](#wiring-the-repository)

`DoctrineOutboxRepository` requires a Doctrine DBAL `Connection`, an `IntegrationEventTranslators` collection, and a `PayloadSerializers` collection. The table layout defaults to table `outbox_events` with BINARY(16) identity columns.

```
