Skip to main content

Documentation 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.

HexCore’s EventBus is the publish-subscribe backbone that decouples domain event producers from their consumers. A single publish() call fans the event out to every subscribed handler, regardless of whether those handlers run in-process or in remote workers. HexCore ships four implementations — in-memory for testing and simple deployments, and Redis Streams, PostgreSQL LISTEN/NOTIFY, and RabbitMQ for distributed production workloads.

The AbstractEventBus port

All implementations satisfy the AbstractEventBus interface defined in hexcore.domain.cqrs.buses:
# hexcore/domain/cqrs/buses.py (simplified)
import abc
import typing as t
from hexcore.domain.events import DomainEvent

class AbstractEventBus(abc.ABC):
    @abc.abstractmethod
    def subscribe(
        self,
        event_type: type[DomainEvent],
        handler: t.Callable[[DomainEvent], t.Awaitable[None]],
    ) -> None:
        """Register a handler for an event type."""
        raise NotImplementedError

    @abc.abstractmethod
    async def publish(self, event: DomainEvent) -> None:
        """Publish an event to all subscribed handlers."""
        raise NotImplementedError

# Alias
IEventBus = AbstractEventBus

DomainEvent base class and fields

DomainEvent is defined in hexcore.domain.events. All domain events inherit from it and gain these auto-populated fields:
event_id
UUID
Auto-generated unique identifier for this event instance. Useful for deduplication and tracing.
occurred_on
datetime
UTC timestamp set at the moment the event is constructed.
event_name
str (computed)
Derived from the class name by stripping the "Event" suffix and uppercasing. For example, UserCreatedEvent"USERCREATED". Used as the routing key in RabbitMQ and Redis.

Defining a domain event

from hexcore.domain.events import DomainEvent

class OrderPlacedEvent(DomainEvent):
    order_id: str
    customer_id: str
    total_amount: float

Four implementations

The simplest implementation — all handlers run in the same process. Use this for local development, unit tests, and applications that don’t need distributed event delivery.
from hexcore.application.cqrs.in_memory_buses import InMemoryEventBus
from app.events import OrderPlacedEvent

bus = InMemoryEventBus()

# Subscribe a handler
async def on_order_placed(event: OrderPlacedEvent) -> None:
    print(f"New order: {event.order_id}")

bus.subscribe(OrderPlacedEvent, on_order_placed)

# Publish
await bus.publish(OrderPlacedEvent(
    order_id="ord-1",
    customer_id="cust-42",
    total_amount=49.99,
))
InMemoryEventBus also supports Smart Routing — if a subscribed handler is decorated with @background_handler, the bus will forward the event payload to the configured ITaskEnqueuer instead of calling the handler directly.
from hexcore.application.cqrs.in_memory_buses import InMemoryEventBus

bus = InMemoryEventBus(
    enqueuer=my_celery_enqueuer,
    serializer=pydantic_serializer,
)

Subscribing multiple handlers to one event

You can subscribe as many handlers as needed to a single event type. All handlers are called for each published event.
bus.subscribe(OrderPlacedEvent, send_confirmation_email)
bus.subscribe(OrderPlacedEvent, update_inventory)
bus.subscribe(OrderPlacedEvent, log_audit_trail)

Lifecycle in FastAPI

# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
import asyncio

from app.events import OrderPlacedEvent
from app.handlers import on_order_placed
from app.bus import event_bus  # your configured bus instance

@asynccontextmanager
async def lifespan(app: FastAPI):
    event_bus.subscribe(OrderPlacedEvent, on_order_placed)
    consuming_task = asyncio.create_task(event_bus.start_consuming())
    yield
    await event_bus.stop()
    consuming_task.cancel()

app = FastAPI(lifespan=lifespan)
InMemoryEventBus has no start_consuming() method — it executes handlers synchronously inline. Only Redis, Postgres, and RabbitMQ buses require an explicit consumer loop.

Build docs developers (and LLMs) love