Expand description
modrun — a modular application composer for Rust.
Build large Tokio services as domain-oriented Modules with constructor
injection, explicit module boundaries, and coordinated lifecycle — wired once
at the composition root.
Register constructors, pull the graph with invokers, and manage start/stop
via Lifecycle.
§Quick start
use modrun::{Hook, Lifecycle, Modrun};
#[derive(Clone)]
struct Config { port: u16 }
#[derive(Clone)]
struct Server { cfg: Config }
impl Hook for Server {
async fn on_start(&mut self) -> modrun::Result<()> {
println!("listening on {}", self.cfg.port);
Ok(())
}
async fn on_stop(&mut self) -> modrun::Result<()> {
Ok(())
}
}
fn new_config() -> Config { Config { port: 8080 } }
fn new_server(cfg: Config) -> Server { Server { cfg } }
fn boot(lc: Lifecycle, server: Server) -> modrun::Result<()> {
lc.append(server)
}
#[tokio::main]
async fn main() -> modrun::Result<()> {
Modrun::builder()
.provide(new_config)
.provide(new_server)
.invoke(boot)
.run()
.await
}Dependencies must be Clone to inject by value. Singletons are cached by
type and cloned on inject. Prefer Arc<T> when a type is large, shared across
many constructors, or injected more than once — Arc<T> is also stored, so you
can inject Arc<T> even when T itself is not Clone. Put shared mutable
state behind Arc inside your type, e.g. #[derive(Clone)] struct Db(Arc<DbInner>).
Because the cache key is the type, a type can only be provided once. To wire two of the same thing — a primary and a replica pool, say — give each its own newtype wrapper.
§Async constructors
Anything that needs to await before it exists (connection pools, clients
that handshake) can be provided directly:
use modrun::Modrun;
#[derive(Clone)]
struct Pool;
async fn connect() -> Result<Pool, std::io::Error> {
Ok(Pool)
}
#[tokio::main]
async fn main() -> modrun::Result<()> {
Modrun::builder()
.provide_result_async(connect)
.invoke(|_pool: Pool| {})
.start()
.await?
.stop()
.await
}§Domain modules
A Module groups related wiring under a name and gives it a private scope.
provide_private keeps a type invisible outside the module, so two modules
can each hold their own Repo without colliding.
use modrun::{Modrun, Module};
#[derive(Clone)]
struct UserRepo;
#[derive(Clone)]
struct UserService;
fn new_user_repo() -> UserRepo { UserRepo }
fn new_user_service(_repo: UserRepo) -> UserService { UserService }
fn boot_user(_svc: UserService) {}
fn user_domain() -> Module {
Module::builder("user")
.provide_private(new_user_repo)
.provide(new_user_service)
.invoke(boot_user)
}
#[tokio::main]
async fn main() -> modrun::Result<()> {
Modrun::builder()
.module(user_domain())
.start()
.await?
.stop()
.await
}§Groups
Several modules can each contribute one T; consumers inject Group<T>
(not Vec<T>). Register members with ModrunBuilder::provide_group. An
empty group needs ModrunBuilder::init_group (or
ModrunBuilder::require_group, which also registers the group and then
rejects emptiness). Members do not occupy the singleton slot for T. Group
members and injected groups require T: Clone; prefer Arc<Group<T>> or
Group<Arc<T>> when many consumers or heavy values would otherwise clone.
§Dependency graph
Export the wiring graph with ModrunBuilder::render_dot or
.dot_graph(path) (DOT is written after validation,
before constructors run). Built-in Lifecycle and Shutdowner are omitted from
the exported graph.
§Startup banner
ModrunBuilder::run and ModrunBuilder::start print a modrun ASCII banner
to stderr before wiring begins (Spring Boot style), and only when stderr is a
TTY. Override with .banner(text) (always printed to
stderr) or disable with .no_banner(). Tests that
run in a terminal should call .no_banner() so captured stderr stays quiet.
§Logging
Framework events (provide / supply / invoke / construct / lifecycle) are
emitted through tracing with target modrun, using uber/fx-style
console lines such as [modrun] PROVIDE .... Events also carry structured
fields (constructor, module, elapsed, …) for JSON subscribers.
Call modrun::logging::init() from examples and local binaries
(requires the logging feature); it is a no-op if a subscriber is already
installed, writes to stderr, and enables ANSI only when stderr is a terminal. Production
processes should install their own subscriber and
filter with RUST_LOG=modrun=info. Without a subscriber the events are
cheap no-ops. Debug builds also print to stderr if a RunningApp is dropped
without RunningApp::stop.
modrun::logging::init();§Stability
The public model is type-keyed singletons, Group<T> value groups, lazy
provide, invoke as the graph root, and start/stop via Lifecycle.
Domain wiring uses Module::builder (symmetric with Modrun::builder).
There are no string qualifiers and no service locator after build. Hook
methods will only grow with defaults. Constructors and invokers accept at most
eight parameters; pack extra deps in a struct. MSRV is 1.85 (edition 2024).
Application-facing API is stable since 1.0.0; see CONTRIBUTING.md.
Startup overhead is mostly your constructors; the container is dropped after build.
Prefer Arc<T> and Arc<Group<T>> for heavy deps — see README「Performance」.
Examples may need a newer compiler via their own dependencies (the MSRV job
does not compile --all-targets).
Application code should use ModrunBuilder::provide /
ModrunBuilder::invoke. Wrapper libraries use the hidden [__wiring] module
and ModrunBuilder::provide_dyn / invoke_dyn (also #[doc(hidden)]).
§Crate features
signal(enabled by default) — OS signal listeners inModrunBuilder::runon Unix (Ctrl-C / SIGTERM) and Windows (Ctrl-C / Ctrl-Break / Ctrl-Close / Ctrl-Shutdown). On other targets the feature is a no-op for OS signals; useShutdowner. Disable withdefault-features = falsewhen you only callModrunBuilder::startor wait onShutdowneryourself.logging—modrun::logging::init/try_initfor fx-style console output. Skip it in production and install your own subscriber.
Modules§
- logging
- Fx-style console logging for modrun framework events.
Structs§
- Group
- Many values of the same type, aggregated from
provide_groupregistrations across modules. - HookFn
- Ad-hoc start/stop callbacks. Prefer implementing
Hookwhen the two phases share state. - Lifecycle
- Application lifecycle registry.
- Modrun
- Entry point for configuring an application:
Modrun::builder. - Modrun
Builder - Fluent builder for a modrun application.
- Module
- A named domain module.
- Multiple
Stop Error - Several OnStop hook failures aggregated into one error.
- Prepared
Task - Await
prepareduring OnStart, then spawnrunin the background. - Running
App - A started application that can be stopped explicitly.
- Shutdowner
- Triggers graceful shutdown from inside the app.
- Stopped
- Completes when the matching
Task/PreparedTaskbegins OnStop (or is dropped). - Task
- Long-running work spawned in OnStart and joined in OnStop.
Enums§
- Error
- Errors produced while wiring, starting, or stopping an application.
Constants§
- DEFAULT_
BANNER - Default ASCII banner printed at startup unless
crate::ModrunBuilder::no_banneris called. - DEFAULT_
TIMEOUT - Default build/start/stop timeout (15s).
Traits§
- Hook
- Start/stop callbacks for a component.
Functions§
- hook
- Build an ad-hoc
Hookfrom closures. - task
- Spawn
runas a background task from OnStart, signal it on OnStop, then join. - task_
with - Like
task, butprepareis awaited during OnStart.
Type Aliases§
- BoxError
- Owned user error retained as
std::error::Error::source. - Result
- Result alias used throughout the crate and by hooks / fallible constructors.