Architecture Overview

Architecture Overview

On this page

Most full-stack frameworks ship as one package that pulls in everything. Rainier ships as twenty-four crates, and an application depends on exactly the ones it uses. An app that wants the queue but not HTTP pays for neither the router nor hyper.

That is not tidiness for its own sake. In Rust, a dependency you do not use is still a dependency you compile, and a crate that reaches sideways into another is a crate you cannot test in isolation. Splitting them forced every seam to be a real one.

The crates

Every crate depends on rainier-support; those edges are left out of the graph because they say nothing. Everything else is drawn.

graph TD
    support[rainier-support<br/><i>Error, futures, type-maps, inflection</i>]

    ormmacros["rainier-orm-macros<br/><i>#[derive(Entity)]</i>"]
    orm[rainier-orm<br/><i>the DBAL: entities, queries, DDL</i>]

    container[rainier-container<br/><i>IoC, providers, facades</i>]
    config[rainier-config<br/><i>config repository, .env</i>]
    events[rainier-events<br/><i>the hook bus</i>]
    http[rainier-http<br/><i>requests, responses, uploads</i>]
    view[rainier-view<br/><i>the template engine</i>]

    crypt[rainier-crypt<br/><i>ciphers, signing, keys, hashing</i>]
    drivers[rainier-drivers<br/><i>Redis, Memcached, Kafka, AWS, SQL executors</i>]

    middleware[rainier-middleware<br/><i>the pipeline</i>]
    routing[rainier-routing<br/><i>routes, groups, URLs</i>]
    validation[rainier-validation<br/><i>rules, contracts</i>]
    database[rainier-database<br/><i>models, repositories, migrations</i>]
    cache[rainier-cache<br/><i>the cache port, drivers, locks</i>]
    scheduler[rainier-scheduler<br/><i>cron, tasks, without-overlapping</i>]
    filesystem[rainier-filesystem<br/><i>local, memory, S3/R2</i>]

    session[rainier-session<br/><i>the bag, stores, middleware</i>]
    auth[rainier-auth<br/><i>guards, gates</i>]
    queue[rainier-queue<br/><i>jobs, drivers, worker</i>]
    mail[rainier-mail<br/><i>mailables, transports</i>]
    notify[rainier-notify<br/><i>notifications, channels</i>]
    broadcast[rainier-broadcast<br/><i>channels, drivers, channel auth</i>]
    websocket[rainier-websocket<br/><i>socket handlers, rooms</i>]
    metrics[rainier-metrics<br/><i>registry, exposition, timing</i>]
    openapi[rainier-openapi<br/><i>the document</i>]
    telemetry[rainier-telemetry<br/><i>trace context, OTLP</i>]
    server[rainier-server<br/><i>kernel, hyper</i>]
    console[rainier-console<br/><i>the console kernel</i>]

    framework[rainier-framework<br/><i>facades, bootstrap, prelude</i>]

    ormmacros --> orm
    orm --> drivers

    http --> middleware
    middleware --> routing
    http --> routing
    http --> validation

    orm --> database
    drivers --> database
    events --> database

    drivers --> cache
    drivers --> filesystem

    cache --> scheduler
    container --> scheduler

    http --> session
    middleware --> session
    database --> session
    cache --> session
    crypt --> session
    orm --> session

    session --> auth
    http --> auth
    middleware --> auth
    database --> auth
    orm --> auth

    container --> queue
    events --> queue
    database --> queue
    drivers --> queue
    orm --> queue

    view --> mail
    events --> mail

    mail --> notify
    support --> broadcast
    http --> websocket
    middleware --> metrics
    routing --> openapi
    middleware --> telemetry

    routing --> server
    middleware --> server
    container --> server

    container --> console

    config --> framework
    routing --> framework
    server --> framework
    auth --> framework
    queue --> framework
    mail --> framework
    notify --> framework
    broadcast --> framework
    websocket --> server
    metrics --> framework
    openapi --> framework
    telemetry --> framework
    console --> framework
    validation --> framework
    cache --> framework
    crypt --> framework
    session --> framework
    scheduler --> framework
    filesystem --> framework
    support --> framework

    classDef umbrella fill:#2d3748,stroke:#4a5568,color:#fff
    classDef shared fill:#535,stroke:#a8a,color:#fff
    class framework umbrella
    class drivers,orm shared

Two crates are shaded because they sit under several others.

rainier-orm is the DBAL itself — a hard fork, vendored rather than depended on, so the framework and the ORM version together. It has exactly one dependency, its own derive macro, and no optional ones at all: that is what keeps it compiling for wasm32 with no feature dance.

rainier-drivers holds transports and nothing else — Redis, Memcached, the AWS SDKs, and the SQL executors that implement the ORM's Executor port. Redis is wanted by the cache, the session store and the queue; without a shared home each would carry its own client, its own protocol code, and its own URL parsing, so an application would configure Redis three times and open three sets of connections to one server. The paradigm is written up in that crate's own module docs: service interfacing lives in drivers; the port crates are adapters over it.

CrateOwns
rainier-supportError, Result, BoxFuture, type-maps, string inflection
rainier-ormthe DBAL: entities, queries, DDL, sharding, the Executor port
rainier-orm-macros#[derive(Entity)]
rainier-containerthe IoC container, providers, lifecycle hooks, facades
rainier-configthe config repository and .env
rainier-eventsthe event dispatcher
rainier-httprequests, responses, cookies, uploads, extractors
rainier-middlewarethe handle(request, next) pipeline, the stack, built-ins
rainier-http-clientthe outbound client, its retry policy and its fake
rainier-routingroute declaration, groups, resources, URL generation
rainier-validationrules, the validator, request contracts
rainier-viewthe template engine
rainier-databaseRainier ORM integration, models, repositories
rainier-authguards, user providers, gates, abilities, challenges
rainier-queuejobs, queue drivers, the worker
rainier-mailmailables, the mailer, transports
rainier-notifynotifications, notifiables, channels
rainier-broadcastbroadcast channels, drivers, subscription authorisation
rainier-websocketthe socket handler contract, the socket handle, rooms
rainier-metricsthe Prometheus registry, its text format, request timing
rainier-openapithe OpenAPI document, and rules-to-schema
rainier-telemetryW3C trace context, and OTLP behind a feature
rainier-serverthe HTTP kernel and the hyper server
rainier-consolethe console: commands, arguments, the kernel
rainier-cryptencryption, signing, key rotation, password hashing
rainier-sessionthe session bag, stores, the middleware
rainier-cachethe cache port, its drivers, and atomic locks
rainier-schedulercron expressions, the schedule, and its locks
rainier-filesystemfile storage: local, memory, S3/R2
rainier-driversRedis, Memcached, AWS and SQL transports
rainier-frameworkfacades, bootstrap, built-in commands, the prelude

Applications depend on rainier-framework and get the rest re-exported — rainier_framework::http, rainier_framework::auth, and so on.

The rules the graph obeys

It is a DAG. No cycles, checked by cargo on every build.

Nothing reaches sideways. routing does not know auth exists. mail does not know an HTTP request is involved. database does not know a request is involved either. This is what makes the queue usable in a binary with no router in it.

The rule holds at the route, which is the place it looks like it should not:

router.get("/me", me).middleware(Authenticate::<User>::resolved_with_guard("api"));

Route::middleware takes impl IntoMiddlewareStack, which bottoms out at dyn Middleware — a trait in rainier-middleware, which routing already depends on. The application names Authenticate<User>, because the application is the crate that knows both the guard and the user model. The router never names a concrete middleware, so it never needs the crate defining one.

This used to be a string ("auth:api") resolved through a registry, on the theory that the dependency was unavoidable otherwise. It was not. See Middleware.

Ports are traits, adapters are types. Every place the framework talks to the outside world is a trait an application can implement:

PortShips withYours would be
ConnectionRainier ORM executorsa driver you wrote
Cachememory, Redis, Redis Cluster, Memcachedyour own
SessionStorememory, database, cache, cookieRedis Streams, whatever
Encrypter / Signerfive AEAD ciphers, HMAC, Ed25519a KMS
ViewEngineTemplateEngine, MemoryEngineTera, Askama, whatever
TransportLogTransport, FileTransport, MemoryTransportSMTP, SES, Postmark
QueueSyncQueue, MemoryQueue, DatabaseQueue, RedisQueue, SqsQueueRabbitMQ, NATS
GuardTokenGuard, SessionGuardOAuth, mTLS
UserProviderRepositoryUserProviderLDAP, an upstream API
HasherArgon2Hasherbcrypt, for legacy rows
ExceptionRendererDefaultExceptionRendereryour error pages
Middlewarea dozen built-insyours
Commandfour built-insyours

Every component ships a double, and every double refuses to let an assertion pass vacuously. Dispatcher::fake(), QueueManager::fake(), Mailer::fake() and MemoryConnection all panic if you assert against a real instance instead of a recording one — because a test that asserts assert_nothing_sent() against a live mailer passes for the wrong reason. See Testing.

What is built on what

Rainier is a framework, not a database library. The ORM underneath it is Rainier ORM, which supplies:

  • #[derive(Entity)] — table and column metadata from a struct
  • repo:: — generic CRUD
  • Query<E> — a fluent builder
  • Executor — the port a driver implements
  • Dialect — SQL rendering, via sea-query
  • shard routing — ShardRoute, ShardCodec, stable_hash

Rainier adds the framework-shaped layer on top: Model, Repository, Criteria, Migrator, lifecycle hook events, and a Connection port that keeps every future Send. That last one has a story — Database: the Send story.

HTTP is hyper 1.x with http-body. Templates, validation, the container, the router and the queue are Rainier's own.

Reading the source

Every module's doc comment explains why it is shaped the way it is, not just what it does. When these pages say "see the module", that is what they mean — the reasoning lives next to the code that made the call, so it stays true when the code changes.

cargo doc --workspace --no-deps --open

This page is the framework’s docs/architecture.md at 7b8cb15. Found a mistake? Edit it on GitHub.

Type to search 2.0.0 docs.