Database Schema
O Sagaweaw persiste todo o estado em PostgreSQL, MySQL 8+ ou H2. O Flyway seleciona automaticamente a migration correta com base no banco detectado ({vendor}). Aqui está o schema completo.
Diagrama de Entidades
┌─────────────────┐ ┌─────────────────┐
│ sagas │───────│ saga_steps │
└─────────────────┘ └─────────────────┘
│
│ ┌─────────────────┐
├────────│ saga_events │
│ └─────────────────┘
│
│ ┌─────────────────┐
├────────│ outbox_messages │
│ └─────────────────┘
│
│ ┌─────────────────┐
└────────│ dead_letters │
└─────────────────┘
Tabelas
sagas
Tabela principal que armazena o estado de cada saga em execução.
CREATE TABLE sagas (
id VARCHAR(36) NOT NULL,
name VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL,
context_json JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL,
completed_at TIMESTAMP(6) WITH TIME ZONE,
idempotency_key VARCHAR(255),
version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX idx_sagas_idempotency ON sagas (idempotency_key)
WHERE idempotency_key IS NOT NULL;
CREATE INDEX idx_sagas_status ON sagas (status);
CREATE INDEX idx_sagas_name ON sagas (name);
CREATE INDEX idx_sagas_created_at ON sagas (created_at);
| Coluna | Tipo | Descrição |
|---|---|---|
id | VARCHAR(36) | UUID da saga |
name | VARCHAR | Nome da saga (ex: "pix-payment") |
status | VARCHAR | STARTED, EXECUTING, COMPLETED, COMPENSATING, COMPENSATED, FAILED |
context_json | JSONB | Contexto serializado (imutável entre steps) |
version | INTEGER | Versão para optimistic locking |
idempotency_key | VARCHAR | Chave para evitar execução duplicada |
saga_steps
Estado de cada etapa dentro de uma saga.
CREATE TABLE saga_steps (
id VARCHAR(36) NOT NULL,
saga_id VARCHAR(36) NOT NULL,
step_name VARCHAR(255) NOT NULL,
step_order INTEGER NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
attempt INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 3,
next_retry_at TIMESTAMP(6) WITH TIME ZONE,
last_error TEXT,
error_trace TEXT,
input_payload JSONB,
output_payload JSONB,
executed_at TIMESTAMP(6) WITH TIME ZONE,
completed_at TIMESTAMP(6) WITH TIME ZONE,
duration_ms BIGINT,
PRIMARY KEY (id),
CONSTRAINT fk_saga_steps_saga FOREIGN KEY (saga_id) REFERENCES sagas (id)
);
CREATE INDEX idx_saga_steps_saga_id ON saga_steps (saga_id);
CREATE INDEX idx_saga_steps_retry ON saga_steps (status, next_retry_at);
| Coluna | Tipo | Descrição |
|---|---|---|
step_name | VARCHAR | Nome do step (ex: "validate-dict") |
step_order | INTEGER | Ordem de execução (0, 1, 2…) |
status | VARCHAR | PENDING, EXECUTING, COMPLETED, FAILED, COMPENSATING, COMPENSATED |
attempt | INTEGER | Tentativa atual |
max_attempts | INTEGER | Máximo de tentativas (habilita display "tentativa X/Y") |
next_retry_at | TIMESTAMP | Quando o próximo retry está agendado |
last_error | TEXT | Mensagem do último erro |
error_trace | TEXT | Stack trace completo do último erro |
input_payload | JSONB | Contexto serializado na entrada do step |
output_payload | JSONB | Contexto serializado na saída (usado pelo compensador) |
duration_ms | BIGINT | Duração da execução em milissegundos |
saga_events
Log imutável de eventos para auditoria completa. Só recebe INSERT, nunca UPDATE.
CREATE TABLE saga_events (
id VARCHAR(36) NOT NULL,
saga_id VARCHAR(36) NOT NULL,
step_name VARCHAR(255),
event_type VARCHAR(100) NOT NULL,
payload JSONB,
created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_saga_events_saga FOREIGN KEY (saga_id) REFERENCES sagas (id)
);
CREATE INDEX idx_saga_events_saga_id ON saga_events (saga_id);
CREATE INDEX idx_saga_events_created ON saga_events (created_at);
| Coluna | Tipo | Descrição |
|---|---|---|
event_type | VARCHAR | SAGA_STARTED, STEP_STARTED, STEP_COMPLETED, STEP_FAILED, COMPENSATION_STARTED, etc. |
payload | JSONB | Dados do evento (mensagem de erro, duração, etc.) |
Consulte o histórico de eventos de uma saga via API:
GET /api/sagas/{id}/events
outbox_messages
Mensagens para o padrão Transactional Outbox. Sem FK em saga_id para suportar arquivamento de sagas.
CREATE TABLE outbox_messages (
id VARCHAR(36) NOT NULL,
saga_id VARCHAR(255) NOT NULL,
step_name VARCHAR(255),
topic VARCHAR(255) NOT NULL,
payload JSONB NOT NULL,
headers JSONB,
published BOOLEAN NOT NULL DEFAULT FALSE,
publish_attempts INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL,
published_at TIMESTAMP(6) WITH TIME ZONE,
PRIMARY KEY (id)
);
CREATE INDEX idx_outbox_unpublished ON outbox_messages (published, created_at);
| Coluna | Tipo | Descrição |
|---|---|---|
topic | VARCHAR | Tópico Kafka destino |
headers | JSONB | Headers adicionais da mensagem |
publish_attempts | INTEGER | Tentativas de publicação |
published | BOOLEAN | Se já foi publicado |
dead_letters
Sagas que esgotaram as tentativas e precisam de intervenção manual. Sem FK em saga_id para persistir o forense mesmo após remoção da saga.
CREATE TABLE dead_letters (
id VARCHAR(36) NOT NULL,
saga_id VARCHAR(255) NOT NULL,
step_name VARCHAR(255) NOT NULL,
error_message TEXT,
error_trace TEXT,
context_snapshot TEXT,
created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL,
reprocessed BOOLEAN NOT NULL DEFAULT FALSE,
reprocessed_at TIMESTAMP(6) WITH TIME ZONE,
reprocessed_by VARCHAR(255),
PRIMARY KEY (id)
);
CREATE INDEX idx_dead_letters_saga_id ON dead_letters (saga_id);
CREATE INDEX idx_dead_letters_unreproc ON dead_letters (reprocessed, created_at);
| Coluna | Tipo | Descrição |
|---|---|---|
error_trace | TEXT | Stack trace completo |
context_snapshot | TEXT | Snapshot do contexto da saga no momento da falha |
reprocessed | BOOLEAN | Se foi reprocessado via API ou manualmente |
reprocessed_by | VARCHAR | Identificação de quem reprocessou ("api" ou usuário) |
Migrations por Banco
O Sagaweaw usa Flyway com o placeholder {vendor} para selecionar a migration correta automaticamente:
resources/
└── db/migration/sagaweaw/
├── postgresql/
│ └── V1__sagaweaw_schema.sql ← JSONB, TIMESTAMP(6) WITH TIME ZONE
├── mysql/
│ └── V1__sagaweaw_schema.sql ← JSON, DATETIME(6), TINYINT(1)
└── h2/
└── V1__sagaweaw_schema.sql ← JSON, TIMESTAMP WITH TIME ZONE
Nenhuma configuração extra é necessária — basta apontar o datasource.url para o banco desejado.