Smart Routing lets you move commands, event handlers, and generic tasks to background worker queues by adding a single decorator — no manual queue calls, no serialization boilerplate, no conditional dispatch logic scattered through your codebase. HexCore’s buses detect the routing decorators at dispatch time and automatically delegate to the configuredDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/Indroic/HexCore/llms.txt
Use this file to discover all available pages before exploring further.
ITaskEnqueuer, keeping your application code clean and queue-agnostic.
The three routing decorators
All three decorators live inhexcore.domain.cqrs.decorators and work by setting special attributes on the class or function they decorate.
@background_command(queue="...")
Marks a Command subclass so that InMemoryCommandBus always routes it to a background queue instead of executing the handler in-process.
__cqrs_background__ = True, it calls enqueuer.enqueue_command(command_name, payload, queue) and returns None immediately — the handler runs asynchronously in a worker process.
@background_handler(queue="...")
Marks an event handler function so that InMemoryEventBus (and RedisEventBus) routes that specific handler to a background queue. Other handlers subscribed to the same event are unaffected.
@background_task(queue="...")
Marks a generic async function as a background task, independent of CQRS. Register it with the worker and enqueue it via ITaskEnqueuer.enqueue_task().
How the bus detects routing
InsideInMemoryCommandBus.dispatch(), the routing check is:
ITaskEnqueuer interface
The ITaskEnqueuer port (in hexcore.domain.cqrs.task_queues) defines four enqueue methods that any adapter must implement:
Worker-side: CQRSConsumer
On the worker side, CQRSConsumer receives the serialized payload and dispatches it to the local in-memory buses, which have the actual handlers registered.
Complete wiring example with Celery
# app/commands.py
from hexcore.domain.cqrs.commands import Command
from hexcore.domain.cqrs.decorators import background_command
@background_command(queue="emails")
class SendWelcomeEmailCommand(Command):
user_id: str
to_address: str
# app/handlers.py
from hexcore.domain.cqrs.handlers import AbstractCommandHandler
from app.commands import SendWelcomeEmailCommand
class SendWelcomeEmailHandler(AbstractCommandHandler[SendWelcomeEmailCommand, None]):
async def handle(self, command: SendWelcomeEmailCommand) -> None:
# This runs inside the Celery worker, not in the web process
await send_email(command.to_address, "Welcome!")
The application bus needs an
enqueuer and serializer to send tasks to Celery. The handler does not need to be registered here — it is registered in the worker.# app/buses.py
from hexcore.application.cqrs.in_memory_buses import InMemoryCommandBus
from hexcore.application.cqrs.registry import HandlerRegistry
from hexcore.infrastructure.cqrs.pydantic_serializer import PydanticSerializer
# Application-side registry has NO handler for background commands
app_registry = HandlerRegistry()
# Build the enqueuer (see celery_app.py below)
from app.celery_app import celery_enqueuer
serializer = PydanticSerializer()
command_bus = InMemoryCommandBus(
registry=app_registry,
enqueuer=celery_enqueuer,
serializer=serializer,
)
# app/celery_app.py
from celery import Celery
from hexcore.domain.cqrs.task_queues import ITaskEnqueuer
import asyncio
celery_app = Celery("myapp", broker="redis://localhost:6379/0")
class CeleryEnqueuer(ITaskEnqueuer):
async def enqueue_command(self, command_name: str, payload: dict, queue: str) -> None:
celery_app.send_task(
"hexcore.process_command",
kwargs={"payload": payload},
queue=queue,
)
async def enqueue_event(self, event_name: str, payload: dict, queue: str) -> None:
celery_app.send_task(
"hexcore.process_event",
kwargs={"payload": payload},
queue=queue,
)
async def enqueue_handler(self, handler_name: str, payload: dict, queue: str) -> None:
celery_app.send_task(
"hexcore.process_handler",
kwargs={"handler_name": handler_name, "payload": payload},
queue=queue,
)
async def enqueue_task(self, task_name: str, payload: dict, queue: str) -> None:
celery_app.send_task(
"hexcore.process_task",
kwargs={"task_name": task_name, "payload": payload},
queue=queue,
)
celery_enqueuer = CeleryEnqueuer()
# app/worker.py
from celery import Celery
from hexcore.application.cqrs.registry import HandlerRegistry
from hexcore.application.cqrs.in_memory_buses import InMemoryCommandBus, InMemoryEventBus
from hexcore.infrastructure.cqrs.pydantic_serializer import PydanticSerializer
from hexcore.infrastructure.workers.consumer import CQRSConsumer
from app.commands import SendWelcomeEmailCommand
from app.handlers import SendWelcomeEmailHandler
# Worker-side registry has the actual handlers
worker_registry = HandlerRegistry()
worker_registry.register_command_handler(
SendWelcomeEmailCommand, SendWelcomeEmailHandler()
)
serializer = PydanticSerializer()
worker_command_bus = InMemoryCommandBus(registry=worker_registry)
worker_event_bus = InMemoryEventBus()
consumer = CQRSConsumer(
command_bus=worker_command_bus,
event_bus=worker_event_bus,
serializer=serializer,
)
celery_app = Celery("myapp", broker="redis://localhost:6379/0")
@celery_app.task(name="hexcore.process_command")
def process_command(payload: dict):
import asyncio
asyncio.run(consumer.process_command(payload))
@celery_app.task(name="hexcore.process_handler")
def process_handler(handler_name: str, payload: dict):
import asyncio
asyncio.run(consumer.process_handler(handler_name, payload))
@celery_app.task(name="hexcore.process_task")
def process_task(task_name: str, payload: dict):
import asyncio
asyncio.run(consumer.process_task(task_name, payload))
# app/routers/auth.py
from fastapi import APIRouter, Depends
from hexcore.domain.cqrs.buses import ICommandBus
from app.buses import command_bus
from app.commands import SendWelcomeEmailCommand
router = APIRouter()
@router.post("/register")
async def register(user_id: str, email: str):
# Returns None immediately — email is sent in background worker
await command_bus.dispatch(
SendWelcomeEmailCommand(user_id=user_id, to_address=email)
)
return {"status": "registered"}
Using @background_handler on event subscribers
@background_handler embeds func.__cqrs_handler_name__ as the fully-qualified module path (e.g. "app.events.notify_customer_sms"). The worker resolves this string via importlib at execution time, so the function must be importable from the worker process.Decorator attributes reference
| Decorator | Sets on target | Value |
|---|---|---|
@background_command(queue) | cls.__cqrs_background__ | True |
@background_command(queue) | cls.__cqrs_queue__ | queue name string |
@background_handler(queue) | func.__cqrs_background_handler__ | True |
@background_handler(queue) | func.__cqrs_queue__ | queue name string |
@background_handler(queue) | func.__cqrs_handler_name__ | "module.qualname" |
@background_task(queue) | func.__cqrs_background_task__ | True |
@background_task(queue) | func.__cqrs_queue__ | queue name string |
@background_task(queue) | func.__cqrs_task_name__ | "module.qualname" |