Why Is It Designed This Way?
1. Why are handlers ordinary async functions?
They already have the essential shape: arguments in, future out. TaskFn
adapts that shape to Tower Service, FromRequest constructs typed context
arguments, and IntoResponse normalizes results. The caller writes application
logic without a macro, base class, or runtime-specific handler trait.
2. What does the user have to own, borrow, or clone?
The worker owns its backend and handler. A job payload is moved into the
handler. Extractors borrow the Task only while constructing owned arguments.
Shared application data must be cloneable and is commonly an Arc<T>; mutable
shared state needs an explicit lock, channel, or pool. Retry additionally
requires a cloneable request because it must own another attempt.
3. Why separate storage/backend concerns from worker execution?
Durable queues differ in encoding, IDs, locks, acknowledgement, polling, and
maintenance. Execution differs in readiness, concurrency, middleware, tracking,
events, and shutdown. Backend connects the two through associated types and
poll methods, while Backend::Layer inserts storage-specific lifecycle policy.
Either side can evolve without copying the other.
4. How does the builder make invalid configurations harder to create?
WorkerBuilder changes type as the backend and layers are added. build is
available only when backend types, task types, handler extraction, and the final
Tower service agree. Missing or incompatible pieces become trait-bound errors
instead of latent runtime branches.
5. Which types and traits appear publicly?
The common surface is WorkerBuilder, Worker, Monitor, Backend, Task,
WorkerContext, Data, FromRequest, IntoResponse, TaskSink, retry/error
types, and Tower Service/Layer. Backend capability traits expose optional
operations without forcing every backend into one enormous interface.
6. How are job errors, retries, shutdown, and middleware represented?
Errors are typed at their source and erased only at composition boundaries.
Retry, limits, timeouts, tracing, panic catching, and acknowledgement are Tower
layers. Shutdown is a cloneable future and admission state. Tracking and
Backend::poll_close turn shutdown into a drain protocol rather than a boolean
checked only at loop exit.
7. Where does Apalis use generics versus trait objects?
Generics describe the hot, homogeneous pipeline: backend, codec, task, handler, extractors, service, layer stack, executor, and policy. Trait objects appear where runtime heterogeneity is intentional: boxed errors, listeners, monitor worker factories/futures, and selected erased streams. Static by default, dynamic at collection and failure boundaries.
8. How does the simplest caller experience compare with the internals?
The caller sees an async function and a short builder chain. Internally that function becomes a generic Tower service whose input is decoded from a backend, augmented by typed extensions, guarded by readiness, executed among unordered in-flight futures, observed by policy layers, acknowledged durably, tracked through cancellation, and drained by a monitor.
That asymmetry is the design achievement. The complexity was not deleted; it was concentrated behind contracts that let the ordinary path remain ordinary.