Architecture Overview

May 20, 2026 ยท View on GitHub

Sendium is a Quarkus-based SMS gateway that accepts outbound messages from HTTP or downstream SMPP clients, normalizes them into internal messages, routes them through configurable routing tables, and delivers them through SMPP client workers to upstream SMSCs or carriers.

The runtime is split into two Maven modules:

ModulePurpose
sendium-appRunnable Quarkus application, Docker image entry point, application properties, and packaging.
sendium-coreGateway implementation: HTTP API, SMPP server/client workers, routing, queues, DLR tracking, configuration support, and webhook forwarding.

Runtime Components

flowchart LR
    apps["Internal applications<br/>CRMs, websites, IoT"]
    downstream[Downstream SMPP clients]
    http["HTTP API<br/>Kannel-compatible /sendsms"]
    smppServer["SMPP server workers<br/>smppserver instances"]
    routerQueue["Router queue<br/>InMemoryQueueProvider"]
    router["Routing manager<br/>StandardRoutingManager"]
    workerManager["Worker manager<br/>StandardOutgoingWorkerHandler"]
    workerQueues[Worker queues]
    smppClients["SMPP client workers<br/>smppclient instances"]
    carriers["Upstream SMSCs<br/>carriers or SMPP providers"]
    dlrStore["DLR correlation store<br/>InMemoryDlrService"]
    webhooks["HTTP webhooks<br/>DLR and MO callbacks"]
    config["Runtime config files<br/>credentials.yml<br/>smsg.properties<br/>routingTable.conf"]

    apps --> http
    downstream --> smppServer
    http --> routerQueue
    smppServer --> routerQueue
    routerQueue --> router
    router --> workerQueues
    workerQueues --> smppClients
    workerQueues --> smppServer
    smppClients --> carriers
    carriers --> smppClients
    smppServer --> downstream
    smppClients --> dlrStore
    dlrStore --> webhooks
    smppClients --> webhooks
    config --> http
    config --> smppServer
    config --> workerManager
    config --> router

Message Flow

Sendium uses a queue-based pipeline. Inbound protocols create StandardMessage instances and enqueue them into the router queue. Router threads evaluate routingTable.conf and enqueue the message into one or more selected worker queues. Worker threads then perform the protocol-specific delivery work.

flowchart TD
    inbound[Inbound message]
    normalize[Normalize to StandardMessage]
    routerQueue[Router queue]
    route[Evaluate routingTable.conf]
    target{Target type}
    table["Routing table<br/>continue evaluation"]
    worker[Worker queue]
    deliver[Deliver through worker]
    retry[Retry or re-enqueue]
    fail[Failed routing queue]

    inbound --> normalize
    normalize --> routerQueue
    routerQueue --> route
    route --> target
    target -->|Table| table
    table --> route
    target -->|Worker| worker
    worker --> deliver
    deliver -->|temporary failure| retry
    retry --> routerQueue
    route -->|no route| retry
    route -->|unexpected failure| fail

HTTP Submission Flow

The HTTP API exposes a Kannel-compatible GET /sendsms endpoint. KannelResource validates HTTP credentials from credentials.yml, decodes request parameters into a StandardMessage, saves initial DLR state with the callback URL when present, and enqueues the message for routing.

sequenceDiagram
    participant Client as HTTP client
    participant API as KannelResource
    participant Creds as CredentialFileWatcher
    participant DLR as InMemoryDlrService
    participant Queue as Router queue
    participant Router as StandardRoutingManager
    participant Worker as SmppClientWorker
    participant SMSC as Upstream SMSC

    Client->>API: GET /sendsms
    API->>Creds: Validate HTTP credentials
    API->>DLR: Save initial message state
    API->>Queue: Enqueue StandardMessage
    API-->>Client: 202 Accepted with gateway UUID
    Router->>Queue: Dequeue message
    Router->>Router: Match routing rules
    Router->>Worker: Enqueue to selected worker
    Worker->>SMSC: submit_sm
    SMSC-->>Worker: submit_sm_resp
    Worker->>DLR: Link gateway UUID to operator message ID

SMPP Server Flow

SMPP server workers accept binds from downstream SMPP clients. The server validates credentials, applies connection/window limits, converts submitted PDUs into internal messages, and puts them into the same router queue used by HTTP submissions.

sequenceDiagram
    participant Client as Downstream SMPP client
    participant Server as SmppServerWorker
    participant Auth as BasicSmppAuthenticationProvider
    participant Submit as BasicSubmitSmProcessor
    participant Queue as Router queue
    participant Router as StandardRoutingManager

    Client->>Server: bind_transceiver / bind_transmitter
    Server->>Auth: Validate SMPP credentials
    Auth-->>Server: Bind accepted or rejected
    Client->>Server: submit_sm
    Server->>Submit: Validate and convert PDU
    Submit->>Queue: Enqueue StandardMessage
    Server-->>Client: submit_sm_resp
    Router->>Queue: Dequeue and route message

Routing And Workers

StandardOutgoingWorkerHandler reads enabled outSms.instance.* definitions from configuration and starts the matching worker implementation. StandardRoutingManager keeps a routing target map containing configured routing tables and active workers.

ComponentResponsibility
InMemoryQueueProviderProvides the shared router queue and named worker queues.
StandardRoutingManagerConsumes the router queue and evaluates routing rules from the default routing table.
StandardOutgoingWorkerHandlerStarts, stops, and tracks configured worker instances.
AbstractOutWorkerBase worker behavior: queue subscription, thread count, TPS limiting, retry actions, pause/suspend behavior, and filters.
SmppClientWorkerSends routed messages to upstream SMSCs through SMPP sessions.
SmppServerWorkerAccepts downstream SMPP clients and routes inbound submit messages or DLRs.

DLR Handling

Outbound HTTP messages can include a Kannel-style dlr-url. Sendium stores the gateway message ID and later links it to the operator/SMSC message ID returned by the SMPP provider. When a DLR arrives, the DLR service resolves the correlation and forwards the callback.

sequenceDiagram
    participant SMSC as Upstream SMSC
    participant Worker as SmppClientWorker
    participant Tracker as InMemoryMessageTracker
    participant Store as InMemoryDlrService
    participant Router as Router queue
    participant DLRHook as ForwardDlrService
    participant App as Originating application

    SMSC->>Worker: deliver_sm delivery receipt
    Worker->>Tracker: createAndEnqueueDLR
    Tracker->>Store: Resolve operator message ID
    Store->>DLRHook: Forward DLR callback if URL exists
    DLRHook->>App: HTTP GET callback
    Tracker->>Router: Enqueue internal MSG_DLR

MO Handling

Mobile-originated messages received from upstream SMPP providers are handled by the SMPP client worker. If the worker instance has an MO forwarding URL configured, SmppClientWorker forwards the MO through ForwardMoService using the configured forwarding format.

sequenceDiagram
    participant SMSC as Upstream SMSC
    participant Worker as SmppClientWorker
    participant Config as smsg.properties
    participant MOHook as ForwardMoService
    participant App as MO webhook endpoint

    SMSC->>Worker: deliver_sm MO
    Worker->>Worker: parseMoAndCreateResponse
    Worker->>Config: Read forward.mo.url and forward.mo.format
    alt forward.mo.url is configured
        Worker->>MOHook: Forward MO context
        MOHook->>App: HTTP POST as JSON or FORM
    else no MO URL configured
        Worker->>Worker: Skip HTTP forwarding
    end
    Worker-->>SMSC: deliver_sm_resp

MO forwarding is configured per SMPP client worker instance in smsg.properties. The implementation lives in sendium-core/src/main/java/gr/cytech/sendium/core/smpp/client/SmppClientWorker.java, where forward.mo.url, forward.mo.format, and parseMoAndCreateResponse control the forwarding behavior.

outSms.instance.testRoute.forward.mo.url = https://example.com/mo
outSms.instance.testRoute.forward.mo.format = JSON

forward.mo.format supports JSON and FORM. If the configured value is invalid, Sendium falls back to JSON.

Configuration And Reloading

Sendium expects runtime files in the configured conf directory.

FileUsed byNotes
credentials.ymlHTTP API and SMPP server authenticationWatched and reloaded by CredentialFileWatcher.
smsg.propertiesQuarkus and Sendium worker/runtime settingsDefines enabled workers, SMPP binds, logging, queue, retry, and forwarding settings.
routingTable.confRouting managerWatched and reloaded by RoutingFileWatcher. Invalid reloads retain the previous routing state.

Persistence Boundaries

Most runtime queues are in-memory. The DLR correlation service uses H2 MVStore at data/dlr-mvstore.db by default and falls back to in-memory maps if the store cannot be opened.

This means operators should treat queued, in-flight messages as process-local state, while DLR correlation has lightweight local persistence.