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:
| Class | Role |
|---|
BaseEntity | Aggregate root base with id, created_at, updated_at, is_active |
DomainEvent | Immutable 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 |
EntityDeletedEvent | Lifecycle event fired when an entity is removed |
IBaseRepository[T] | Port declaring get_by_id, list_all, save, delete |
IUnitOfWork | Port for transactional boundary (commit, rollback, collect_entity) |
Command | Immutable 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 handlers —
AbstractCommandHandler[TCommand, TResult] processes one command type
- Query handlers —
AbstractQueryHandler[TQuery, TResult] answers one query type
- Handler registry —
HandlerRegistry maps command/query types to their handlers
- In-memory buses —
InMemoryCommandBus, 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:
| Adapter | Technology | Extra |
|---|
BaseSQLAlchemyRepository | SQLAlchemy async sessions | hexcore[sql] |
BeanieODMCommonImplementationsRepo | MongoDB via Beanie ODM | hexcore[mongo] |
InMemoryCommandBus / InMemoryQueryBus | Pure Python, no dependencies | core |
RedisEventBus | Redis Streams pub/sub | hexcore[redis] |
PostgresEventBus | PostgreSQL LISTEN/NOTIFY | hexcore[sql] |
RabbitMQ worker | AMQP via aio-pika | hexcore[rabbitmq] |
CeleryEnqueuer | Celery task queue | hexcore[celery] |
ProcrastinateEnqueuer | Procrastinate task queue | hexcore[procrastinate] |
RedisLockProvider | Distributed cron locking | hexcore[redis] |
| Redis / memory cache backends | Key-value cache | core / hexcore[redis] |
Project structures
HexCore’s CLI can scaffold two standard layouts. Choose the one that fits your team’s preference.
Hexagonal layout
Vertical-slice layout
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",
}
)
The vertical-slice layout organises code by feature rather than by layer. Every feature owns its full stack from domain to infrastructure. Best for larger teams or microservices where feature isolation is more important than layer isolation.hexcore init my_project --template vertical-slice
my_project/
├── config.py
├── src/
│ ├── features/
│ │ ├── users/
│ │ │ ├── domain/
│ │ │ │ ├── entities.py
│ │ │ │ ├── repositories.py
│ │ │ │ └── events.py
│ │ │ ├── application/
│ │ │ │ ├── commands.py
│ │ │ │ └── queries.py
│ │ │ └── infrastructure/
│ │ │ ├── models.py
│ │ │ └── repositories.py
│ │ └── orders/
│ │ └── ...
│ └── shared/
│ ├── domain/ # Cross-cutting domain primitives
│ ├── application/ # Shared buses, registry
│ └── infrastructure/ # Shared DB session, cache
└── migrations/
A matching config.py:from hexcore.config import ServerConfig
config = ServerConfig(
repository_discovery_paths={
"src.features.users.infrastructure.repositories",
"src.features.orders.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.