SEDA bus · Java · 1.3.1

The original — and the only port with guaranteed delivery.

seda-bus-java is where the design started, and it's the version embedded in service-bus and 1M5. It carries features none of the other six ports have: optional AtLeastOnce/ExactlyOnce persistence, datatype channels, and a pull model alongside the push-based consumer API.

Worker pool
Executors.newFixedThreadPool, daemon threads
True stage parallelism
Yes — platform threads
Dependencies
ra-common only
Envelope
ra.common.Envelope, shared with service-bus and 1M5
Routing slip
DynamicRoutingSlip — a LIFO stack
Concurrency permits
java.util.concurrent.Semaphore, one per channel
Guaranteed delivery
Yes — AtMostOnce / AtLeastOnce / ExactlyOnce
Batch size
64 envelopes per drain
Back-pressure default
Reject (the only port not defaulting to Block)
Race/sanitizer verified
Not run

How it implements the design

Everything the platform gives you, used directly.

The envelope, completion callback, service-level, and channel/consumer/bus interfaces all come from ra-common, so the bus is a drop-in message bus for the wider stack. The routing slip is a genuine LIFO stack (producers push routes and call ratchet(); completion pops) — the other six ports use a FIFO list for the same itinerary concept.

Guaranteed delivery is per-channel or per-envelope: AtLeastOnce writes the envelope to the channel's on-disk store via an atomic temp-file move before send returns, removes it on ack, and replays unprocessed envelopes in filename-time order; ExactlyOnce adds a bounded dedup map so replay skips duplicates. “Exactly once” means processing effectively once, not a distributed two-phase commit. Datatype channels let a channel carry a class filter, rejecting envelopes whose content isn't assignable to it — a feature none of the other six ports have.

Concurrency permits are real java.util.concurrent.Semaphores, one per channel, held by the worker pool — the JVM's own primitive, not a hand-rolled one. History matters here: 1.3.0 replaced a 100ms scan loop (which capped throughput at roughly 10 messages/sec/channel) with the current event-driven drain, and added retry/dead-letter and persistence in the same pass.

Results

Never a cliff — because the JVM already pays the cost these ports had to fix by hand.

Capacity curve — sustained throughput under real, bounded back-pressure
ConfigSustained eps0.5×1.0×1.5×
cap1648,3410.930.860.65
cap8444,3180.940.800.46
Firehose throughput (envelopes/sec, unthrottled — diagnostic, not capacity planning)
Configepsvs. seq
seq (1 producer)795,225
par (8 producers, 1 channel)450,4840.57×
chan (8 producers, 8 channels)2,028,4092.55×

Reading the numbers

The design choice that explains it

cap8 collapses hardest of any port at 1.5× overload (0.46, versus 0.65 for its own cap1) — consistent with ExecutorService-style shared-pool contention once eight stages genuinely oversubscribe the pool.

But par's latency never shows the cliff several other ports pay for — p50 1,659.5µs, p99 7,307.2µs, p999/max within one order of magnitude, despite a design similar to what Rust and C++ had before their fixes. The leading explanation: the JVM's ExecutorService/LockSupport spin briefly before making an OS-level park/wake call, while Rust's raw std::sync::mpsc and C++'s original single mutex parked immediately with no spin phase. Rust's fix (parking_lot, which adds the same spin-before-park behavior) and C++'s fix (a two-lock queue, a different mechanism entirely) both closed most of the gap to Java's behavior here — without either port copying Java's approach directly. Java simply never had the problem to begin with.

The full cross-language results, including every anomaly explained

Source

Read the code, or the rest of the family.

seda-bus-java on GitHub — source, its own DESIGN.md, CHANGELOG.md, and test suite.