Magento 2 e Outbox Pattern: integrazioni affidabili con sistemi esterni

Integrare Magento 2 con ERP, OMS, CRM, PIM, sistemi di pagamento, middleware e servizi esterni è una delle attività più comuni nei progetti ecommerce enterprise.
Ma cosa succede quando Magento salva correttamente un ordine nel database e, subito dopo, la chiamata verso il sistema esterno fallisce?
Oppure quando un messaggio viene inviato a RabbitMQ, ma la transazione Magento che avrebbe dovuto generarlo viene successivamente annullata?
Sono problemi tipici dei sistemi distribuiti.
Una soluzione architetturale particolarmente efficace è il Transactional Outbox Pattern, comunemente chiamato semplicemente Outbox Pattern.
In questo articolo vediamo perché è utile nelle integrazioni Magento 2, quale problema risolve e come implementarlo in un modulo Adobe Commerce/Magento Open Source.
Il problema: Magento e i sistemi esterni non condividono la stessa transazione
Consideriamo un caso molto comune.
Quando viene creato un ordine dobbiamo:
- salvare l’ordine in Magento;
- notificare l’ordine a un ERP.
Una prima implementazione potrebbe essere concettualmente simile a questa:
$orderRepository->save($order);
$erpClient->sendOrder($order);
Sembra ragionevole.
Il problema è che abbiamo due operazioni indipendenti:
Magento Database
+
External ERP
Non esiste una transazione ACID che comprenda entrambe.
Possiamo quindi trovarci in questa situazione:
SAVE ORDER
↓
COMMIT DATABASE
↓
CALL ERP
↓
TIMEOUT
Magento contiene l’ordine.
L’ERP no.
Il sistema è entrato in uno stato inconsistente.
Il problema del Dual Write
In architettura distribuita questo scenario viene generalmente chiamato dual write problem.
Un’operazione di business deve modificare due sistemi differenti:
Database Magento
+
Sistema esterno
e vorremmo che entrambe le operazioni fossero atomiche.
Idealmente:
BEGIN TRANSACTION
SAVE ORDER
SEND ORDER TO ERP
COMMIT
Ma il database Magento non può effettuare il rollback di una richiesta HTTP già elaborata dall’ERP.
Allo stesso modo, l’ERP non partecipa normalmente alla transazione MySQL di Magento.
Il problema diventa ancora più evidente se utilizziamo un message broker.
Supponiamo di fare:
$orderRepository->save($order);
$publisher->publish(
'order.created',
$message
);
Abbiamo comunque due sistemi:
MySQL
+
RabbitMQ
Anche in questo caso le operazioni non sono automaticamente atomiche.
Perché pubblicare l’evento prima del commit non risolve il problema
Potremmo provare a pubblicare il messaggio prima del commit:
BEGIN TRANSACTION
SAVE ORDER
PUBLISH EVENT
↓
RabbitMQ
COMMIT
Ma immaginiamo che RabbitMQ riceva correttamente il messaggio e successivamente il database effettui un rollback.
Avremo:
RabbitMQ → OrderCreated #123
Magento → Order #123 NON ESISTE
Il consumer potrebbe quindi elaborare un evento relativo a uno stato che non è mai stato realmente confermato nel sistema sorgente.
Pubblicare dopo il commit non basta
Possiamo invertire il problema:
BEGIN TRANSACTION
SAVE ORDER
COMMIT
PUBLISH EVENT
Ora siamo sicuri che l’ordine esista.
Ma cosa succede se il processo PHP termina subito dopo il commit?
COMMIT
↓
PHP PROCESS CRASH
↓
EVENT NOT PUBLISHED
L’ordine esiste nel database, ma nessun sistema esterno ne viene informato.
Abbiamo eliminato un problema creandone un altro.
Il Transactional Outbox Pattern
L’Outbox Pattern cambia completamente l’approccio.
Invece di cercare di aggiornare contemporaneamente database e sistema esterno, registriamo l’intenzione di inviare il messaggio nello stesso database della transazione principale.
L’architettura diventa:
┌──────────────────────┐
│ Magento 2 │
│ │
│ Business Operation │
└──────────┬───────────┘
│
TRANSACTION
│
┌───────────┴───────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ sales_order │ │ outbox │
│ │ │ │
│ Order #123 │ │ OrderCreated │
└───────────────┘ └───────────────┘
│ │
└───────────┬───────────┘
│
COMMIT
L’ordine e l’evento vengono salvati nella stessa transazione database.
Questa è la caratteristica fondamentale del pattern.
Se la transazione fallisce:
ROLLBACK
↓
Order → NON salvato
Outbox → NON salvato
Se invece la transazione viene confermata:
COMMIT
↓
Order → salvato
Outbox → salvato
Non può esistere uno senza l’altro, purché entrambi partecipino realmente alla stessa transazione.
Il Message Relay
A questo punto non abbiamo ancora inviato nulla all’ERP.
Abbiamo soltanto registrato l’evento.
Serve quindi un secondo componente:
Message Relay
Il suo compito è leggere gli eventi pendenti dalla tabella outbox.
Magento Transaction
│
▼
┌──────────────┐
│ Outbox │
└──────┬───────┘
│
│ read pending events
▼
┌──────────────┐
│ Message Relay│
└──────┬───────┘
│
▼
RabbitMQ
│
▼
Consumer
│
▼
ERP
Il relay può essere implementato attraverso:
- cron Magento;
- CLI command;
- background worker;
- consumer dedicato;
- processo esterno;
- Change Data Capture, in architetture più avanzate.
In una prima implementazione Magento, cron o worker sono spesso sufficienti.
Progettare una Outbox Table in Magento 2
Possiamo creare una tabella dedicata:
devlogica_outbox_event
con una struttura concettuale simile:
event_id
event_uuid
event_type
aggregate_type
aggregate_id
payload
status
attempts
created_at
available_at
processed_at
last_error
Per esempio:
| Campo | Valore |
|---|---|
| event_uuid | 550e8400-e29b-41d4-a716-446655440000 |
| event_type | order.created |
| aggregate_type | order |
| aggregate_id | 123 |
| status | pending |
| attempts | 0 |
| created_at | 2026-09-14 09:30:00 |
Il payload potrebbe essere JSON:
{
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "order.created",
"order_id": 123,
"increment_id": "000000123",
"store_id": 1
}
Declarative Schema Magento 2
La tabella può essere creata tramite db_schema.xml.
Un esempio semplificato:
<table name="devlogica_outbox_event"
resource="default"
engine="innodb"
comment="Transactional Outbox Events">
<column xsi:type="bigint"
name="event_id"
unsigned="true"
nullable="false"
identity="true"
comment="Event ID"/>
<column xsi:type="varchar"
name="event_uuid"
nullable="false"
length="36"
comment="Event UUID"/>
<column xsi:type="varchar"
name="event_type"
nullable="false"
length="255"
comment="Event Type"/>
<column xsi:type="varchar"
name="aggregate_type"
nullable="false"
length="100"
comment="Aggregate Type"/>
<column xsi:type="varchar"
name="aggregate_id"
nullable="false"
length="255"
comment="Aggregate ID"/>
<column xsi:type="text"
name="payload"
nullable="false"
comment="Event Payload"/>
<column xsi:type="varchar"
name="status"
nullable="false"
length="32"
default="pending"
comment="Status"/>
<column xsi:type="int"
name="attempts"
unsigned="true"
nullable="false"
default="0"
comment="Attempts"/>
<column xsi:type="timestamp"
name="created_at"
nullable="false"
default="CURRENT_TIMESTAMP"
comment="Created At"/>
<column xsi:type="timestamp"
name="available_at"
nullable="true"
comment="Available At"/>
<column xsi:type="timestamp"
name="processed_at"
nullable="true"
comment="Processed At"/>
<column xsi:type="text"
name="last_error"
nullable="true"
comment="Last Error"/>
<constraint xsi:type="primary"
referenceId="PRIMARY">
<column name="event_id"/>
</constraint>
<constraint xsi:type="unique"
referenceId="DEVLOGICA_OUTBOX_EVENT_UUID">
<column name="event_uuid"/>
</constraint>
<index referenceId="DEVLOGICA_OUTBOX_STATUS_AVAILABLE"
indexType="btree">
<column name="status"/>
<column name="available_at"/>
</index>
</table>
In produzione gli indici devono naturalmente essere progettati in funzione del volume e delle query effettuate dal relay.
Outbox Writer
È utile isolare completamente la creazione degli eventi.
Per esempio:
interface OutboxWriterInterface
{
public function add(
string $eventType,
string $aggregateType,
string $aggregateId,
array $payload
): void;
}
Una possibile implementazione:
final class OutboxWriter implements OutboxWriterInterface
{
public function __construct(
private readonly ResourceConnection $resource,
private readonly SerializerInterface $serializer
) {
}
public function add(
string $eventType,
string $aggregateType,
string $aggregateId,
array $payload
): void {
$connection = $this->resource->getConnection();
$connection->insert(
$this->resource->getTableName(
'devlogica_outbox_event'
),
[
'event_uuid' => Uuid::uuid4()->toString(),
'event_type' => $eventType,
'aggregate_type' => $aggregateType,
'aggregate_id' => $aggregateId,
'payload' => $this->serializer->serialize($payload),
'status' => 'pending',
'attempts' => 0,
]
);
}
}
Il punto importante non è tanto la classe in sé.
È dove viene eseguita questa INSERT.
La Outbox deve partecipare alla stessa transazione
Questo è il dettaglio che distingue un vero Transactional Outbox da una semplice tabella utilizzata come coda.
L’operazione:
SAVE BUSINESS DATA
e:
INSERT OUTBOX EVENT
devono appartenere alla stessa transazione database.
Concettualmente:
$connection->beginTransaction();
try {
// modifica stato applicativo
$this->processBusinessOperation();
// registra evento
$this->outboxWriter->add(
'order.export.requested',
'order',
(string)$orderId,
$payload
);
$connection->commit();
} catch (\Throwable $exception) {
$connection->rollBack();
throw $exception;
}
Se qualcosa fallisce, entrambe le modifiche vengono annullate.
Non mettere la chiamata HTTP nella transazione
Un errore importante sarebbe fare:
$connection->beginTransaction();
try {
$orderRepository->save($order);
$erpClient->sendOrder($order);
$connection->commit();
} catch (\Throwable $e) {
$connection->rollBack();
}
Tecnicamente possiamo mantenere aperta la transazione mentre effettuiamo la chiamata HTTP.
Architetturalmente è una pessima idea.
Una richiesta esterna potrebbe impiegare:
50 ms
500 ms
5 secondi
30 secondi
TIMEOUT
Durante questo periodo manteniamo aperta una transazione database, aumentando potenzialmente lock contention, tempi di risposta e rischio di failure.
Con Outbox, la transazione deve essere breve:
BEGIN
↓
business update
↓
insert outbox
↓
COMMIT
La comunicazione lenta viene spostata fuori dalla transazione.
Outbox + RabbitMQ in Magento 2
Magento/Adobe Commerce dispone già di un Message Queue Framework.
Possiamo quindi utilizzare l’Outbox come livello di affidabilità prima della pubblicazione sul broker.
L’architettura diventa:
MAGENTO
│
DB TRANSACTION
│
┌────────┴────────┐
│ │
▼ ▼
sales_order outbox
│ │
└──────COMMIT─────┘
│
▼
Outbox Publisher
│
▼
RabbitMQ
│
▼
Magento Consumer
│
▼
ERP
È importante capire che:
RabbitMQ e Outbox non sono alternative.
Risolvono problemi differenti.
RabbitMQ gestisce la comunicazione asincrona.
L’Outbox garantisce che l’intenzione di pubblicare il messaggio sia persistita atomicamente insieme alla modifica di business.
Perché RabbitMQ da solo non risolve il Dual Write
Consideriamo:
$orderRepository->save($order);
$publisher->publish(
'devlogica.order.export',
$message
);
Abbiamo comunque:
DB WRITE
↓
BROKER WRITE
Se il processo muore tra le due operazioni:
Order saved
Message missing
L’Outbox elimina questa finestra.
DB TRANSACTION
Order saved
+
Outbox saved
COMMIT
Dopo il commit il messaggio può essere pubblicato anche qualche secondo dopo.
Abbiamo accettato eventual consistency per ottenere maggiore affidabilità.
Il publisher della Outbox
Un worker può recuperare gli eventi:
SELECT *
FROM devlogica_outbox_event
WHERE status = 'pending'
AND (
available_at IS NULL
OR available_at <= NOW()
)
ORDER BY event_id
LIMIT 100;
Per ogni evento:
READ EVENT
↓
PUBLISH TO RABBITMQ
↓
SUCCESS?
/ \
YES NO
↓ ↓
SENT RETRY
Pseudo-codice:
foreach ($events as $event) {
try {
$publisher->publish(
$event->getEventType(),
$event->getPayload()
);
$repository->markAsProcessed(
$event->getId()
);
} catch (\Throwable $exception) {
$repository->markAsFailed(
$event->getId(),
$exception->getMessage()
);
}
}
Ma anche qui esiste un problema interessante.
Cosa succede se RabbitMQ riceve il messaggio ma Magento crasha?
Consideriamo:
PUBLISH
↓
RabbitMQ receives message
↓
PHP CRASH
↓
markAsProcessed() NON eseguito
Quando il worker riparte trova nuovamente:
status = pending
e ripubblica il messaggio.
Risultato:
MESSAGE #1
MESSAGE #1
Abbiamo un duplicato.
Questo comportamento non è necessariamente un bug.
È una conseguenza normale dei sistemi at-least-once delivery.
L’idempotenza è parte dell’architettura
Un sistema robusto deve assumere che un messaggio possa essere ricevuto più di una volta.
Ogni evento dovrebbe quindi avere un identificatore univoco:
{
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "order.created",
"order_id": 123
}
Il consumer può mantenere una tabella:
processed_messages
con:
event_id
processed_at
Prima di elaborare:
EVENT RECEIVED
↓
event_id already processed?
↓
YES NO
│ │
IGNORE PROCESS
Questo rende il consumer idempotente.
Idempotenza applicativa
In alcuni casi possiamo ottenere idempotenza direttamente attraverso la logica di dominio.
Per esempio:
Create shipment for order #123
potrebbe utilizzare una chiave:
magento-order-123-shipment
Se il sistema esterno riceve nuovamente la stessa richiesta:
POST /shipments
Idempotency-Key:
magento-order-123-shipment
può restituire il risultato precedente senza creare una seconda spedizione.
Questo approccio è particolarmente importante per:
- pagamenti;
- rimborsi;
- spedizioni;
- fatture;
- ordini ERP;
- movimenti finanziari.
Retry
Un’integrazione esterna non deve considerare ogni errore definitivo.
Un ERP potrebbe temporaneamente rispondere:
HTTP 503
oppure:
Connection timeout
In questi casi possiamo riprovare.
Una strategia semplice potrebbe essere:
attempt 1 → immediately
attempt 2 → +1 minute
attempt 3 → +5 minutes
attempt 4 → +15 minutes
attempt 5 → +1 hour
Questa tecnica è chiamata exponential backoff quando l’intervallo cresce progressivamente.
Il campo:
available_at
permette di stabilire quando un evento può essere nuovamente elaborato.
Non tutti gli errori devono essere ritentati
È importante distinguere:
Transient Failure
da:
Permanent Failure
Per esempio:
HTTP 503
Gateway Timeout
Connection refused
sono tipicamente candidati per un retry.
Mentre:
HTTP 400
Invalid SKU
Invalid Customer
Missing Required Field
potrebbero richiedere intervento umano o correzione dei dati.
Un’integrazione enterprise dovrebbe quindi classificare gli errori.
Dead Letter Queue
Dopo un certo numero di tentativi:
attempts >= MAX_ATTEMPTS
il messaggio non dovrebbe continuare a essere elaborato indefinitamente.
Possiamo spostarlo logicamente nello stato:
failed
oppure utilizzare una vera Dead Letter Queue (DLQ) sul broker.
Il flusso diventa:
EVENT
↓
PROCESS
↓
ERROR
↓
RETRY
↓
ERROR
↓
RETRY
↓
ERROR
↓
DLQ
↓
ALERT
Un operatore può quindi analizzare il problema e, dopo averlo risolto, effettuare il replay del messaggio.
Observability: l’integrazione deve essere osservabile
Un sistema affidabile non è semplicemente un sistema che effettua retry.
Deve permettere di capire cosa sta succedendo.
Per ogni evento dovremmo poter rispondere a domande come:
Quando è stato creato?
È stato pubblicato?
Quante volte abbiamo provato?
Qual è stato l'ultimo errore?
Quando verrà effettuato il prossimo retry?
È stato elaborato dall'ERP?
Per questo motivo una Outbox dovrebbe conservare almeno:
event_uuid
event_type
aggregate_id
created_at
status
attempts
available_at
processed_at
last_error
E nei log dovremmo sempre includere:
event_uuid
come correlation identifier.
Correlation ID
Consideriamo un ordine che attraversa:
Magento
↓
RabbitMQ
↓
Integration Service
↓
ERP
Senza un identificatore comune dobbiamo correlare manualmente log provenienti da sistemi diversi.
Con un correlation_id possiamo invece cercare:
CORRELATION_ID = abc-123
in tutta l’infrastruttura.
Un evento più completo potrebbe quindi essere:
{
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"correlation_id": "9b856ac0-99d1-4ac6",
"event_type": "order.created",
"aggregate": {
"type": "order",
"id": "123"
},
"occurred_at": "2026-09-14T09:30:00Z",
"payload": {
"increment_id": "000000123"
}
}
Event Payload: snapshot o riferimento?
Un’altra decisione architetturale importante riguarda il payload.
Possiamo salvare soltanto:
{
"order_id": 123
}
e fare recuperare al consumer lo stato corrente dell’ordine.
Oppure possiamo salvare:
{
"order_id": 123,
"status": "processing",
"total": 125.90,
"currency": "EUR",
"items": [...]
}
I due approcci hanno implicazioni differenti.
Reference
Outbox → order_id
↓
load current order
È più leggero, ma il consumer potrebbe leggere uno stato diverso da quello esistente al momento dell’evento.
Snapshot
Outbox → complete event data
L’evento rappresenta esattamente ciò che è accaduto in quel momento.
Per veri domain event, lo snapshot è spesso preferibile.
Event versioning
Quando un’integrazione vive per anni, il formato degli eventi cambia.
Oggi:
{
"order_id": 123
}
Domani:
{
"order_id": 123,
"store_id": 2
}
È quindi utile introdurre:
{
"event_type": "order.created",
"event_version": 2
}
Il consumer può così gestire versioni differenti senza rompere immediatamente la compatibilità.
Event naming
Anche il naming è importante.
Meglio utilizzare eventi che descrivono qualcosa che è accaduto:
order.created
order.cancelled
shipment.created
invoice.created
refund.created
customer.registered
rispetto a nomi eccessivamente legati all’implementazione:
send_order_to_erp
call_sap
update_external_system
Il primo approccio riduce l’accoppiamento.
Domani lo stesso evento:
order.created
potrebbe essere utilizzato da:
ERP
CRM
Data Warehouse
Marketing Automation
Fraud Detection
senza modificare il dominio Magento.
Outbox Pattern e Magento Events
Magento utilizza ampiamente eventi e observer.
Per esempio possiamo intercettare eventi relativi a:
order
invoice
shipment
customer
product
Ma un observer Magento non equivale automaticamente a un Outbox.
Questo:
public function execute(
Observer $observer
): void {
$order = $observer->getOrder();
$this->erpClient->sendOrder($order);
}
introduce ancora una comunicazione sincrona con il sistema esterno.
Un observer può invece essere utilizzato per registrare un evento nella Outbox, purché sia garantita la corretta partecipazione alla transazione che vogliamo proteggere.
La distinzione è fondamentale:
Magento Event
≠
Transactional Outbox
L’Outbox è una garanzia architetturale sulla persistenza dell’evento.
Outbox Pattern vs Magento Message Queue
Anche questi due concetti non devono essere confusi.
| Funzione | Outbox | Message Queue |
|---|---|---|
| Atomicità con DB | ✓ | non automaticamente |
| Persistenza evento | ✓ | ✓ |
| Comunicazione asincrona | indiretta | ✓ |
| Retry | implementabile | ✓ |
| Scaling consumer | limitato | ✓ |
| Routing | limitato | ✓ |
| Disaccoppiamento | ✓ | ✓ |
La combinazione più robusta è spesso:
Transactional Outbox
+
Message Broker
+
Idempotent Consumer
Un’architettura Magento enterprise
In un ecommerce con diverse integrazioni possiamo arrivare a:
┌─────────────────┐
│ Magento 2 │
└────────┬────────┘
│
DB TRANSACTION
│
┌────────────┴────────────┐
│ │
▼ ▼
BUSINESS DATA OUTBOX
│
▼
OUTBOX RELAY
│
▼
RabbitMQ
│
┌──────────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
ERP Consumer CRM Consumer PIM Consumer
│ │ │
▼ ▼ ▼
ERP CRM PIM
Magento diventa il produttore di eventi.
I sistemi esterni reagiscono agli eventi senza essere direttamente accoppiati alla transazione ecommerce.
Esempio: esportazione ordine verso ERP
Vediamo il flusso completo.
Il cliente completa il checkout:
PLACE ORDER
Magento esegue:
BEGIN TRANSACTION
INSERT sales_order
INSERT sales_order_item
INSERT outbox_event
COMMIT
La Outbox contiene:
order.created
Il relay legge l’evento:
OUTBOX
↓
RabbitMQ
Il consumer ERP riceve:
order.created
e chiama:
ERP API
Se l’ERP risponde correttamente:
SUCCESS
Se l’ERP non è disponibile:
ERROR
↓
RETRY
Dopo il limite massimo:
DLQ
↓
ALERT
Il checkout del cliente non dipende quindi dalla disponibilità momentanea dell’ERP.
Il vantaggio sulla customer experience
Questo aspetto è spesso sottovalutato.
Senza asincronicità:
Checkout
↓
Magento
↓
ERP call
↓
3 seconds
↓
response
il cliente paga direttamente la latenza dell’integrazione.
Con Outbox:
Checkout
↓
Magento transaction
↓
COMMIT
↓
response
e separatamente:
Outbox
↓
RabbitMQ
↓
ERP
Il sistema esterno non è più sul critical path della richiesta del cliente.
Cosa succede se l’ERP rimane offline per due ore?
Con un’integrazione sincrona potremmo avere:
ERP DOWN
↓
integration errors
↓
checkout problems
Con un’architettura asincrona:
ERP DOWN
Magento
↓
Outbox
↓
Queue
↓
Queue
↓
Queue
Quando l’ERP torna disponibile:
ERP UP
↓
Consumers restart processing
↓
backlog drained
Il sistema assorbe quindi temporaneamente il problema.
Questa proprietà viene spesso definita temporal decoupling.
Attenzione all’ordine degli eventi
Supponiamo che Magento generi rapidamente:
OrderCreated
OrderPaid
OrderCancelled
Il consumer dovrebbe idealmente elaborarli nello stesso ordine.
Altrimenti potremmo ricevere:
OrderCancelled
OrderCreated
OrderPaid
con risultati imprevedibili.
È quindi importante valutare:
- ordering;
- partition key;
- aggregate ID;
- sequence number;
- caratteristiche del broker utilizzato.
Una possibile struttura dell’evento può includere:
{
"aggregate_id": "123",
"sequence": 3
}
Il consumer può così identificare eventi fuori sequenza.
Exactly once: attenzione alla promessa
Nei sistemi distribuiti è meglio non progettare l’integrazione assumendo che ogni evento venga elaborato esattamente una volta end-to-end.
Una strategia molto più robusta consiste nel progettare per:
At-least-once delivery
+
Idempotent processing
Il sistema accetta quindi che un evento possa essere consegnato più volte, ma garantisce che elaborarlo nuovamente non produca effetti indesiderati.
Questa filosofia semplifica enormemente la costruzione di integrazioni resilienti.
Cleanup della Outbox
La tabella Outbox cresce continuamente.
Non conviene mantenere indefinitamente tutti gli eventi elaborati nel database operativo Magento.
Possiamo introdurre una retention policy:
processed > 30 days
↓
archive/delete
oppure:
processed > 90 days
↓
archive
La scelta dipende dai requisiti di audit.
Gli eventi failed, invece, potrebbero avere una retention differente.
Monitoraggio
In produzione dovremmo monitorare almeno:
Pending events
Oldest pending event
Events processed/minute
Failed events
Retry count
DLQ size
Consumer lag
Uno degli indicatori più utili è:
Age of oldest pending event
Se normalmente un evento viene elaborato entro pochi secondi e improvvisamente troviamo:
Oldest event = 47 minutes
probabilmente esiste un problema nel relay, nel broker o nel sistema downstream.
Quando utilizzare l’Outbox Pattern in Magento 2
L’Outbox è particolarmente utile quando Magento deve comunicare eventi importanti verso sistemi esterni.
Per esempio:
ERP
OrderCreated
OrderCancelled
InvoiceCreated
ShipmentCreated
RefundCreated
OMS
OrderPlaced
OrderAllocated
OrderCancelled
CRM
CustomerCreated
CustomerUpdated
Warehouse
ShipmentRequested
ReturnRequested
Payment systems
PaymentCaptured
PaymentVoided
RefundRequested
Data platform
OrderCreated
CustomerRegistered
ProductUpdated
Più critica è l’informazione, maggiore è il valore del pattern.
Quando l’Outbox Pattern può essere eccessivo
Non tutte le integrazioni richiedono questa complessità.
Per operazioni non critiche come:
invalidate cache
send analytics event
update optional recommendation data
la perdita occasionale di un evento potrebbe essere accettabile.
L’architettura deve sempre essere proporzionata al rischio.
Se invece stiamo sincronizzando:
ordini
pagamenti
fatture
spedizioni
rimborsi
stock
la consistenza diventa molto più importante.
Outbox Pattern: vantaggi
L’adozione del pattern permette di ottenere diversi benefici.
Affidabilità
Un evento non viene perso semplicemente perché il sistema esterno è temporaneamente indisponibile.
Atomicità locale
Business data ed evento vengono salvati nella stessa transazione database.
Disaccoppiamento
Magento non deve conoscere la disponibilità immediata del sistema downstream.
Performance
Le API esterne vengono rimosse dal critical path delle operazioni utente.
Retry
Gli errori temporanei possono essere recuperati automaticamente.
Observability
Ogni evento possiede uno stato persistente e può essere tracciato.
Scalabilità
L’elaborazione può essere distribuita attraverso message broker e consumer multipli.
Gli svantaggi
Naturalmente il pattern introduce anche complessità.
Dobbiamo gestire:
- tabella Outbox;
- publisher/relay;
- retry;
- idempotenza;
- cleanup;
- monitoring;
- ordering;
- versioning degli eventi;
- gestione degli errori.
Inoltre il sistema diventa eventually consistent.
Magento potrebbe avere già registrato l’ordine mentre l’ERP lo riceverà alcuni secondi dopo.
Nella maggior parte delle integrazioni enterprise questo compromesso è però preferibile a un’integrazione sincrona fragile.
Una possibile architettura di riferimento
Per un progetto Magento 2 con integrazioni enterprise adotteremmo una struttura simile:
Magento Domain
│
▼
Domain/Application Event
│
▼
Transactional Outbox
│
▼
Outbox Publisher
│
▼
RabbitMQ
│
▼
Idempotent Consumer
│
▼
Integration Service
│
▼
ERP / OMS / CRM / External API
accompagnata da:
Retry
Backoff
DLQ
Correlation ID
Event Versioning
Monitoring
Alerting
Non stiamo più semplicemente “chiamando un’API”.
Stiamo progettando una integrazione distribuita affidabile.
Conclusioni
Integrare Magento 2 con un sistema esterno attraverso una semplice chiamata HTTP può funzionare nei progetti più piccoli, ma diventa fragile quando l’integrazione riguarda processi critici come ordini, pagamenti, spedizioni, fatture e stock.
Il problema fondamentale è il dual write:
Magento Database
+
External System
non possono normalmente essere aggiornati all’interno della stessa transazione.
Il Transactional Outbox Pattern risolve il problema cambiando strategia:
Business Data
+
Outbox Event
vengono salvati atomicamente nello stesso database.
Successivamente:
Outbox
↓
Message Broker
↓
Consumer
↓
External System
gestisce la comunicazione asincrona.
Combinando Transactional Outbox, RabbitMQ, retry, idempotenza, DLQ e observability è possibile costruire integrazioni Magento 2 molto più robuste e adatte a contesti enterprise.
Il punto centrale non è evitare completamente gli errori.
Nei sistemi distribuiti gli errori sono inevitabili.
L’obiettivo architetturale è costruire un sistema capace di assorbirli, ritentare le operazioni e recuperare automaticamente senza perdere informazioni.
FAQ
Cos’è l’Outbox Pattern in Magento 2?
Il Transactional Outbox Pattern consiste nel salvare un evento destinato a sistemi esterni nello stesso database e nella stessa transazione della modifica di business che lo ha generato. Un processo separato pubblica successivamente l’evento verso RabbitMQ o un altro sistema di messaging.
Perché usare l’Outbox Pattern con Magento 2?
È particolarmente utile nelle integrazioni con ERP, OMS, CRM, warehouse, sistemi di pagamento e altri servizi esterni, perché riduce il rischio di perdere eventi quando una modifica Magento è stata salvata ma la comunicazione esterna fallisce.
RabbitMQ sostituisce l’Outbox Pattern?
No. RabbitMQ gestisce il messaging asincrono, mentre l’Outbox risolve il problema dell’atomicità tra la modifica dei dati applicativi e l’intenzione di pubblicare un evento. Le due tecnologie possono essere utilizzate insieme.
L’Outbox Pattern garantisce exactly-once delivery?
Non necessariamente. Il relay può pubblicare nuovamente un messaggio se si verifica un errore dopo la pubblicazione ma prima dell’aggiornamento dello stato dell’Outbox. Per questo motivo i consumer dovrebbero essere idempotenti.
Cosa significa consumer idempotente?
Significa che elaborare più volte lo stesso evento produce lo stesso risultato di una singola elaborazione. Una strategia comune consiste nell’assegnare un UUID a ogni evento e registrare gli identificativi già elaborati.
È possibile implementare l’Outbox con RabbitMQ?
Sì. Una soluzione comune consiste nel salvare gli eventi nella Outbox durante la transazione Magento e utilizzare successivamente un relay per pubblicarli sul Message Queue Framework di Magento, utilizzando RabbitMQ come broker.
È necessario utilizzare RabbitMQ?
No. L’Outbox Pattern è indipendente dal broker. Gli eventi possono essere elaborati direttamente da un worker oppure pubblicati verso RabbitMQ, ActiveMQ, Kafka, SQS o altre infrastrutture di messaging, a seconda dell’architettura.
Outbox Pattern e Magento Observer sono la stessa cosa?
No. Un observer è un meccanismo applicativo per reagire agli eventi Magento. L’Outbox Pattern riguarda invece la persistenza transazionale affidabile dell’evento. Un observer può contribuire alla creazione dell’evento Outbox, ma non rende automaticamente l’operazione transazionale.
Come gestire gli errori verso ERP o API esterne?
È consigliabile distinguere gli errori temporanei da quelli permanenti, applicare retry con backoff ai primi e spostare gli eventi non recuperabili in uno stato failed o in una Dead Letter Queue.
Quali integrazioni Magento beneficiano maggiormente dell’Outbox?
Soprattutto quelle relative a ordini, pagamenti, rimborsi, fatture, spedizioni, resi, disponibilità e altre operazioni in cui la perdita di un messaggio può creare inconsistenze di business., Adobe Commerce Developer, Software Architect, Tech Lead, Solution Architect.
Fonti e approfondimenti
Adobe Commerce Developer Documentation — Message Queues
Documentazione ufficiale Adobe relativa al Message Queue Framework, ai consumer e alla configurazione dei broker supportati.
Adobe Commerce — RabbitMQ
Documentazione ufficiale relativa all’utilizzo e alla configurazione di RabbitMQ con Adobe Commerce.
AWS Prescriptive Guidance — Transactional Outbox Pattern
Descrizione del pattern, del dual write problem, delle strategie di implementazione e della necessità di gestire messaggi duplicati e consumer idempotenti.
Microservices.io — Transactional Outbox
Descrizione originale e approfondita del Transactional Outbox Pattern, dei partecipanti all’architettura e del Message Relay.
