Skip to content

Architecture

Retsu is one compiled program with several runtime roles. The API and each worker run as separate processes but use the same configuration, queue rules, database code, cache code, and monitoring setup.

This page describes the current code, not a future design.

Running system

flowchart LR
    Client["Application using Retsu"] --> API["API process<br/>queue HTTP routes"]

    subgraph Retsu["Retsu processes"]
        API
        Expired["Expired-message cleaner"]
        DeadLetter["Dead-letter-message cleaner"]
        State["State-metrics collector"]
        Migrate["Migration job"]
    end

    API --> Local["In-process queue-name cache"]
    Local --> Shared["Shared queue-details cache"]
    Shared --> Database[("PostgreSQL")]
    API --> Database
    Expired --> Database
    DeadLetter --> Database
    State --> Database
    Migrate --> Database

    Prometheus["Prometheus"] -. "scrapes" .-> API
    Prometheus -. "scrapes" .-> Expired
    Prometheus -. "scrapes" .-> DeadLetter
    Prometheus -. "scrapes" .-> State

    API -. "exports traces" .-> Collector["Trace collector"]
    Expired -. "exports traces" .-> Collector
    DeadLetter -. "exports traces" .-> Collector
    State -. "exports traces" .-> Collector

PostgreSQL is the source of truth. The shared Redis-compatible cache stores queue details, and each process can keep queue IDs and immutable names in memory. Queue messages are stored only in PostgreSQL.

The API serves queue requests and handles retries during dequeue. The cleaners remove expired active messages and old dead-letter records. The state collector reads queue counts for Prometheus. The migration role only applies database migrations.

One image, separate processes

The same binary and container image can run:

Role Command
API retsu api
Migration job retsu migrate
Expired-message cleaner retsu worker run queue expired-message-cleaner
Dead-letter-message cleaner retsu worker run queue dead-letter-message-cleaner
State-metrics collector retsu worker run queue state-metrics-collector

Starting one role does not start another. This lets each process restart or scale independently while keeping one implementation of queue behavior.

Request path

sequenceDiagram
    participant Client
    participant HTTP as API handler
    participant Queue as Queue module
    participant Cache as Queue caches
    participant DB as PostgreSQL

    Client->>HTTP: Queue request
    HTTP->>Queue: Validated command
    Queue->>Cache: Read queue metadata when needed
    Cache->>DB: Read on cache miss
    Queue->>DB: Run message transaction
    DB-->>Queue: Result
    Queue-->>HTTP: Domain result or error
    HTTP-->>Client: HTTP response

HTTP handlers translate requests and responses. They do not contain queue rules or SQL. The queue module owns the operation, and PostgreSQL completes message changes in transactions.

Message ownership

Dequeue directly claims a message whose visibility timeout has ended. There is no retry worker. A later dequeue also moves delivery-exhausted messages into dead-letter storage before finding another message.

The cleanup workers operate on the same queue module and database implementation as the API. See Message lifecycle for the complete flow.

Monitoring path

Every process records logs, metrics, and optional traces. The API serves monitoring endpoints on its HTTP port. Each worker starts a separate management server.

The state collector is different from event metrics:

  • enqueue, acknowledgement, expiry, and dead-letter events are recorded by the operation that performs them;
  • current ready and in-flight counts are collected from PostgreSQL;
  • only one state collector is active, while other collector processes can wait for failover.

See Monitoring and Queue state summaries.

Why the system is split this way

  • One queue module keeps HTTP, worker, and database behavior consistent.
  • Separate processes let deployments scale the API and each maintenance job independently.
  • PostgreSQL transactions keep message state and queue-state summaries consistent.
  • Cache failures can fall back to PostgreSQL without changing queue correctness.
  • A dedicated state collector avoids running the same database work in every API replica.
  • One production image reduces differences between migrations, the API, workers, local development, and integration tests.

The tradeoff is that a complete deployment must run several roles and monitor each one. Workers and Deployment and releases list the required commands and endpoints.

Where the code lives

Path Purpose
src/entrypoints/ Starts the selected API, worker, or migration role
src/app/ Builds shared dependencies
src/modules/ Registers application features and their workers
src/modules/queue/ Owns queue API, rules, operations, storage, and workers
src/cache/ Implements the in-memory and Redis-compatible caches
src/database/ Creates and checks PostgreSQL pools
src/observability/ Provides logs, metrics, and traces
src/worker/ Runs one selected worker and its management server
migrations/ Contains forward-only PostgreSQL changes

Continue with the Codebase guide for dependency injection, module boundaries, and change placement.