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.

Hexagonal architecture, also called the ports-and-adapters pattern, structures a system so that the core domain is completely isolated from external concerns such as databases, HTTP frameworks, and message queues. HexCore implements this pattern in Python by providing the abstract ports (interfaces) in the domain and application layers and a collection of concrete adapters in the infrastructure layer, all wired together through dependency injection. This page explains the three layers HexCore defines, the conventions that govern each one, and how to scaffold both supported project structures using the built-in CLI.

The three layers

Domain layer — the heart of your system

The domain layer contains your business concepts and rules. It has no knowledge of databases, HTTP, or any external system. Every class here is a pure Python model or abstract contract. HexCore provides the following domain primitives:
ClassRole
BaseEntityAggregate root base with id, created_at, updated_at, is_active
DomainEventImmutable event base; carries event_id and occurred_on
EntityCreatedEvent[T]Lifecycle event fired when an entity is first persisted
EntityUpdatedEvent[T]Lifecycle event fired when an entity changes
EntityDeletedEventLifecycle event fired when an entity is removed
IBaseRepository[T]Port declaring get_by_id, list_all, save, delete
IUnitOfWorkPort for transactional boundary (commit, rollback, collect_entity)
CommandImmutable write-side intent, auto-stamped with command_id + timestamp
Query[TResult]Read-side request carrying no side effects

Application layer — use-cases and orchestration

The application layer orchestrates domain objects. It knows about domain abstractions (ports) but never imports from infrastructure. Here you will find:
  • Command handlersAbstractCommandHandler[TCommand, TResult] processes one command type
  • Query handlersAbstractQueryHandler[TQuery, TResult] answers one query type
  • Handler registryHandlerRegistry maps command/query types to their handlers
  • In-memory busesInMemoryCommandBus, InMemoryQueryBus, InMemoryEventBus
  • Use-cases — for teams that prefer the classic use-case pattern before migrating to CQRS

Infrastructure layer — adapters that talk to the outside world

Infrastructure adapters implement the domain ports using real technology:
AdapterTechnologyExtra
BaseSQLAlchemyRepositorySQLAlchemy async sessionshexcore[sql]
BeanieODMCommonImplementationsRepoMongoDB via Beanie ODMhexcore[mongo]
InMemoryCommandBus / InMemoryQueryBusPure Python, no dependenciescore
RedisEventBusRedis Streams pub/subhexcore[redis]
PostgresEventBusPostgreSQL LISTEN/NOTIFYhexcore[sql]
RabbitMQ workerAMQP via aio-pikahexcore[rabbitmq]
CeleryEnqueuerCelery task queuehexcore[celery]
ProcrastinateEnqueuerProcrastinate task queuehexcore[procrastinate]
RedisLockProviderDistributed cron lockinghexcore[redis]
Redis / memory cache backendsKey-value cachecore / hexcore[redis]

Project structures

HexCore’s CLI can scaffold two standard layouts. Choose the one that fits your team’s preference.
The classic hexagonal layout groups code by architectural layer. Best for small-to-medium applications where the domain is the primary organising axis.
hexcore init my_project --template hexagonal
my_project/
├── config.py                     # ServerConfig with repository_discovery_paths
├── src/
│   ├── domain/
│   │   └── {module}/
│   │       ├── __init__.py
│   │       ├── entities.py       # BaseEntity subclasses
│   │       ├── repositories.py   # IBaseRepository ports
│   │       ├── services.py       # BaseDomainService subclasses
│   │       ├── value_objects.py  # Immutable value types
│   │       ├── events.py         # DomainEvent subclasses
│   │       ├── enums.py
│   │       └── exceptions.py
│   ├── application/
│   │   └── {module}/
│   │       ├── commands.py       # Command subclasses + handlers
│   │       ├── queries.py        # Query subclasses + handlers
│   │       └── dtos.py           # DTO subclasses
│   └── infrastructure/
│       ├── database/
│       │   ├── models/           # SQLAlchemy ORM models
│       │   └── documents/        # Beanie ODM documents
│       └── repositories/         # IBaseRepository implementations
└── migrations/                   # Alembic migrations
A matching config.py:
from hexcore.config import ServerConfig

config = ServerConfig(
    repository_discovery_paths={
        "src.infrastructure.repositories",
    }
)

How the layers communicate

Dependency always points inward: infrastructure depends on application; application depends on domain; domain depends on nothing external.
┌─────────────────────────────────────────────────┐
│                 Infrastructure                  │
│  SQLAlchemy repo  │  FastAPI router  │  Redis bus│
│       ↓           │       ↓          │     ↓     │
│  ┌──────────────────────────────────────────┐   │
│  │              Application                 │   │
│  │  CommandHandler  │  QueryHandler  │ Bus  │   │
│  │       ↓          │       ↓        │  ↓  │   │
│  │  ┌──────────────────────────────────┐   │   │
│  │  │            Domain                │   │   │
│  │  │  BaseEntity │ IBaseRepository    │   │   │
│  │  │  DomainEvent│ IUnitOfWork        │   │   │
│  │  └──────────────────────────────────┘   │   │
│  └──────────────────────────────────────────┘   │
└─────────────────────────────────────────────────┘
The domain layer defines ports (interfaces). The infrastructure layer provides adapters (implementations). Your application code always imports from the domain, never from infrastructure directly.

Dependency rule in practice

# ✅ CORRECT — application depends on domain port
from hexcore.domain.repositories import IBaseRepository
from hexcore.domain.uow import IUnitOfWork

class CreateUserHandler(AbstractCommandHandler[CreateUserCommand, UserDTO]):
    def __init__(self, uow: IUnitOfWork) -> None:
        self.uow = uow  # injected concrete adapter at runtime

    async def handle(self, command: CreateUserCommand) -> UserDTO:
        async with self.uow:
            repo: IBaseRepository = self.uow.repositories["users"]
            user = User(name=command.name, email=command.email)
            await repo.save(user)
            await self.uow.commit()
            return UserDTO.model_validate(user)
# ❌ WRONG — domain importing from infrastructure breaks isolation
from hexcore.infrastructure.repositories.base import BaseSQLAlchemyRepository

class User(BaseEntity):
    repo = BaseSQLAlchemyRepository(...)  # Never do this
Use Python’s abc.ABC combined with BaseEntity (via AbstractModelMeta) whenever you need an abstract entity base class that is also a Pydantic model. HexCore resolves the metaclass conflict for you.

Build docs developers (and LLMs) love