Requirements
Section titled “Requirements”| Package | Required | Purpose |
|---|---|---|
oridecon | Yes | Core framework |
oridecon-contracts | Yes | Protocol definitions |
redis | Recommended | Redis queue backend |
aio-pika | Optional | RabbitMQ queue backend |
boto3 | Optional | SQS queue backend |
Problem
Section titled “Problem”Applications need to exchange messages between services, decouple producers from consumers, and handle backpressure, retries, and dead-letter routing. oridecon-queue provides a unified, DI-friendly abstraction over six queue backends — memory, Redis, RabbitMQ, Kafka, SQS, Azure Service Bus, and GCP Pub/Sub — so you can switch backends without changing application code.
Mental Model
Section titled “Mental Model”The queue system is structured around three layers:
┌──────────────┐ publish/subscribe ┌──────────────┐│ Producer │ ───────────────────────▶ │ Consumer ││ (any code) │ BusMessage(topic, │ (handler) ││ │ payload, headers) │ │└──────────────┘ └──────────────┘ │ ▲ │ │ ▼ │┌──────────────────────────────────────────────────┐│ QueueProtocol implementations ││ ┌────────┐ ┌──────────┐ ┌───────┐ ┌─────────┐ ││ │ Memory │ │ Redis │ │ Kafka │ │ RabbitMQ│ │ ...│ └────────┘ └──────────┘ └───────┘ └─────────┘ ││ Named multi-backend via DI │└──────────────────────────────────────────────────┘Core Concepts
Section titled “Core Concepts”QueueProtocol
Section titled “QueueProtocol”QueueProtocol (from oridecon.contracts.queue) defines the interface every backend implements:
class QueueProtocol: async def connect(self) -> None: ... async def close(self) -> None: ... async def publish(self, topic: str, message: BusMessage) -> None: ... async def subscribe( self, topic: str, handler: Callable[[BusMessage], Coroutine], ) -> None: ... async def health_check(self, timeout: float = 5.0) -> HealthCheckResult: ...BusMessage
Section titled “BusMessage”Messages are BusMessage instances (from oridecon.contracts.queue):
from oridecon.contracts.queue import BusMessage, DeliveryGuarantee
msg = BusMessage( topic="orders.created", payload={"order_id": "ord-1"}, headers={"source": "api"}, delivery_guarantee=DeliveryGuarantee.AT_LEAST_ONCE,)QueueModule
Section titled “QueueModule”Use QueueModule to register backends into the DI container:
from oridecon.queue import QueueModulefrom oridecon.queue.config import QueueConfig
config = QueueConfig(backends=[...])module = QueueModule.configure(config) # returns DynamicModuleNamed Multi-Backend
Section titled “Named Multi-Backend”You can run multiple queue backends in the same application. Each backend gets registered under its name using Named() injection:
from typing import Annotatedfrom oridecon.contracts.queue import QueueProtocolfrom oridecon.di.markers import Named
class OrderService: def __init__( self, primary: QueueProtocol, # primary backend events: Annotated[QueueProtocol, Named("events")], # named backend ) -> None: ...The primary backend (marked primary=True or the first entry) also gets the unnamed QueueProtocol binding.
In-Process Publish Batching
Section titled “In-Process Publish Batching”BatchedPublisher stages messages in memory and publishes them in a single flush() call:
from oridecon.queue import BatchedPublisherfrom oridecon.contracts.queue import BusMessage
publisher = BatchedPublisher(queue)publisher.stage("orders.created", BusMessage(topic="orders.created", payload=data))await publisher.flush()Note: BatchedPublisher is in-memory only — staged entries are lost if the process restarts or the instance is discarded before flush(). For crash-safe publishing alongside database writes, use the durable SQL outbox inside your own database transaction instead: OutboxStoreProtocol from oridecon.contracts.data.outbox, implemented by SQLOutboxStore, with OutboxPublisher relaying pending rows after commit.
Dead Letter Queue
Section titled “Dead Letter Queue”DeadLetterQueue stores messages that exceeded their retry limit:
from oridecon.queue import DeadLetterQueue
dlq = DeadLetterQueue(queue, max_retries=3)dlq.monitor_failures() # routes to DLQ topicMessage Pipeline
Section titled “Message Pipeline”MessagePipeline chains middleware around message processing:
from oridecon.queue import MessagePipeline, MiddlewareBase
class LoggingMiddleware(MiddlewareBase): async def __call__(self, message, next_handler): logger.info("processing", message_id=message.id) return await next_handler(message)
pipeline = MessagePipeline([LoggingMiddleware()])Typical Usage
Section titled “Typical Usage”import asyncio
from oridecon import Applicationfrom oridecon.contracts.queue import BusMessage, QueueProtocolfrom oridecon.di.module import module, Modulefrom oridecon.queue import QueueModulefrom oridecon.queue.config import QueueConfig, NamedQueueConfig
config = QueueConfig(backends=[ NamedQueueConfig(name="default", driver="memory", primary=True),])
@module(imports=[QueueModule.configure(config)])class AppModule(Module): pass
async def main() -> None: async with Application.boot(name="app", modules=[AppModule]) as app: queue = await app.container.resolve(QueueProtocol)
async def handler(msg: BusMessage) -> None: print(f"Handled: {msg.payload}")
await queue.subscribe("notifications", handler) await queue.publish("notifications", BusMessage( topic="notifications", payload={"text": "Hello"}, )) await asyncio.sleep(0.1)
asyncio.run(main())Best Practices
Section titled “Best Practices”- Always subscribe before publishing in the same process to avoid race conditions (in-memory backend).
- Use named backends when different message types have different delivery requirements (e.g., Redis for fast notifications, Kafka for durable event streaming).
- Set
max_retriesper backend to control dead-letter behavior. - Wrap cleanup in
try/finallywhen manually managingQueueProtocollifecycle. - Use
TransactionOutboxfor reliable multi-service message emission.