Skip to content
Menu

Documentation

Infrastructure

Your modules use a cache, a database or email without knowing which product is behind them. Each kind of infrastructure has an interface, and a driver connects that interface to a concrete backend. Configuration chooses the driver, so the same module code runs against any configured backend and in any deployment profile.

  • An interface is what a module may call, such as cache().get(key). In code and in error messages it is called a capability.
  • A driver implements an interface for one backend, such as redis for the cache.
  • An instance is a named, configured driver, such as the default database and a second analytics one.

What is included

Interface What it is for Drivers
Database your tables, transactions, the outbox and audit sqlalchemy
Cache fast shared data and counters redis
Event bus delivering events between modules streams, redis, memory
Task queue background work celery
Object store files local
Notifications email and other messages log, smtp

Each page shows what a module calls, and each driver's settings and limits. Configure backends shows how to choose them.

What you get by default / what you declare

You get by default You declare
A default instance of every capability, built from the flat settings (DATABASE_URL, CACHE_URL, BROKER_URL, STORAGE_*, EVENT_BUS_BACKEND) extra instances or driver overrides in the INFRA setting
db() / DbSession: the session of the running unit on the module's database Annotated[AsyncSession, Database("analytics")] for a named database
Accessors that return operations, such as cache() and notifier() the instance name, when not default
A startup preflight of the module's needs, without I/O define_module(..., capabilities=[CapabilityRequirement(...)]) for anything beyond the defaults
Refusal of backend SDKs, raw clients and backend constructors in modules nothing

Operations, not handles

An interface offers operations, never the underlying client. What only one engine can do (Redis eval, Celery send_task, a filesystem path) stays behind a driver-only raw_client() / raw_backend(), which business modules may not call.

Drivers are the only code that constructs a backend, and each is built lazily, so nothing opens at import. To connect a new backend, write a driver; for a new kind of infrastructure, add your own capability.

Declare what you need

from sandloom.sdk import define_module
from sandloom.sdk.module import CapabilityRequirement

register = define_module(
    __name__,
    capabilities=[
        CapabilityRequirement("cache", features=frozenset({"cas"})),
        CapabilityRequirement("database", instance="analytics"),
    ],
)

The loader preflights requirements at startup, without I/O: a required one that is unknown, unconfigured or missing a feature fails the module load (and startup, if the module is required); an optional one (required=False) only warns. A module whose tables live in a non-default database declares it as CapabilityRequirement("database", instance="analytics"); migrations read this to choose the database that hosts the module's chain (see Migrations).

What breaks it

Code or error When
SANDLOOM-C003 raw_client() / raw_backend() in module code
SANDLOOM-C004 a backend SDK (boto3, motor, pymongo, neo4j, temporalio, kafka), a legacy global, an infra-internal module or sandloom.driverkit, or a driver package's module, imported by a module
SANDLOOM-C005 a kernel handle (.event_bus, .infra, .service_registry, request.app.state.core) reached in module code
SANDLOOM-C024 a raw non-relational query outside unscoped()
SANDLOOM-B001 a required CapabilityRequirement that is unknown, unconfigured or missing a feature
SANDLOOM-B009 a write route with several writable sessions and no write store
SANDLOOM-B021, SANDLOOM-B022 a selected driver with two providers, or one that cannot be used
SANDLOOM-B023 an experimental capability in production without the accepted risk
ProviderContractError / FeatureUnsupportedError / ExperimentalCapabilityError a driver of the wrong contract, a missing feature, an experimental capability without a driver
Guarantees, precisely
  • Construction (G-INFRA-01, 02). Backends are constructed only by drivers; no implicit default registry.
  • Contracts and features (G-INFRA-03, 04, 05, 07). Enforced by the registry, validated at build.
  • Preflight (G-INFRA-06). A module's requirements are checked at load without I/O.
  • Capabilities (C-INFRA-01 to 11). Driver plugins, synthesized defaults and INFRA, independent databases each with its own unit and outbox, DI resolution by name, experimental capabilities, the log notifier, the key-addressed object store, the operation-based cache, diagnostics and the universal accessor.
  • No cross-database atomicity (G-UOW-07). Two databases are two units; one invariant belongs in one database.

See the guarantee ledger and ADR-0005.

How it works

The composition root builds the registry from the settings and binds it to the core; module-level accessors (cache(), notifier(), ...) resolve through the process's active registry. Outbox and audit writes go through the OutboxStore / AuditStore contracts that the database provider exposes, so the kernel's write path never imports a concrete ORM model. The paradigm-agnostic kernel imports no SQLAlchemy at runtime; an architecture test allowlists only the relational surface.

Design rationale