How your application is shaped
An application built with this framework is one deployable process with one recurring shape:
client ↔ adapter → trait ← implementation
- A client speaks a protocol: a browser, a REST consumer, a gRPC client, a message producer.
- An adapter translates that one protocol into calls on your application.
The framework ships REST (
iet-http-axum) and HTML/HTMX (iet-http-htmx-axum) adapters today; a gRPC or message-consumer adapter would be another crate in the same slot. - A trait says what a capability does. It knows no protocol and no storage.
- An implementation carries the trait out with a real mechanism: a database such as TimescaleDB or PostgreSQL, a remote service call, a published message, a search index such as OpenSearch.
Noteswhat it does — no protocol, no storageThe adapter and the implementation both depend on the trait. Neither knows the other, and neither can replace the other: that is what keeps the capability reusable across applications, clients, and storage engines.
This is application architecture — not framework architecture
The framework's own runtime, contracts, and dependency rules are documented in Architecture. This page is about the app you build with it.
Your app mirrors the framework's integration crates
Look at the framework's crates/integrations/: every crate binds one
transport or one storage engine to a contract — iet-auth-basic-axum (login
endpoint), iet-auth-basic-timescale (credential persistence),
iet-auth-login-htmx-axum (browser sign-in). Your application has exactly that
structure:
| In the framework | In your application |
|---|---|
Core contracts (iet-auth, iet-db, iet-cache) | Your contract crate: types, rules, ports |
Protocol adapters (iet-http-axum, iet-http-htmx-axum) | One adapter crate per client |
Implementations (iet-db-timescale, iet-cache-memory) | One implementation crate per provider |
Stack presets (iet-stack-web-axum) | Your composition root / server crate |
| Fake or in-memory provider | A test double for the same trait |
The contract is the seam. Everything else — database, UI, caller — can be replaced without rewriting the rules between them.
A capability, packaged: contract + implementation + adapter
In application code, we call one such capability a module: code packaged so another application can reuse it. A module usually has three kinds of code, plus a composition step:
| Part | Question | Must not know |
|---|---|---|
| Contract | What does the capability do? | SQL, HTTP, HTML, a concrete database |
| Implementation | How is it carried out? | Which UI or route called it |
| Adapter | How does one caller reach it? | How another adapter reaches it |
| Composition | Which concrete pieces does this app use? | Nothing—it is allowed to know everything |
These can be modules in one crate or separate crates. Splitting into crates is a reuse decision, not a moral requirement.
Module here means application feature
This page uses “module” for an application capability such as tasks or billing.
It is compile-time code organization, not a dynamically loaded plugin. The
framework also has iet_app::Module for supervised process lifecycle; that is a
different, narrower use of the word.
A real module in this repository
The browser starter's task feature follows the same shape:
crates/task-contract/contractTask, TaskChange, rules, Tasks, TaskRepositorycrates/task-impl-timescale/implementationTaskService and SQL repositorycrates/task-adapter-htmx/adapterRoutes, pages, and formsservers/web/compositionSelects concrete implementations and adaptersDependencies point inward: the adapter depends on the contract, the implementation depends on the contract, and neither the contract nor the implementation depends on Axum or HTML. The composition root depends on all of them.
The contract
The contract owns the domain model, input rules, and the ports other code talks
through. This is abbreviated from
examples/htmx-starter/crates/task-contract/src/port.rs:
Two details matter for reuse:
- The actor is an application type (
Principal), not an HTTP header or cookie. - The repository has an associated
Sessiontype. Storage can require a database session without leaking a concrete database into the contract.
The adapter
The HTMX adapter is generic over the ports and supplies routes—not business logic:
// examples/htmx-starter/crates/task-adapter-htmx/src/handlers.rs
The same adapter can be instantiated with a different T in tests or another
application. It does not import the Timescale implementation.
The composition root
Only the application entry point chooses concrete types:
// examples/htmx-starter/servers/web/src/main.rs (composition excerpt)
let router = api
.module?
.finish;
Read it as a sentence: start the app, install the task module's routes, then give those routes the concrete task service. Nothing is discovered by name.
What reuse looks like
| Requirement changes | Reuse | Replace |
|---|---|---|
| Tests instead of PostgreSQL | Contract, service rules, adapter | Repository with a fake or in-memory implementation |
| HTML today, JSON tomorrow | Contract, service rules, storage | task-adapter-htmx with a different adapter |
| A second application needs tasks | The entire module | Composition root and selected modules |
| A different database | Contract and adapters | Timescale implementation |
The important dependency rule is one-way knowledge:
servers/webChooses concrete types oncetask-adapter-htmxKnows one callertask-contractKnows no SQL or HTTPtask-impl-timescaleKnows how the work is doneThe composition root is the only place that knows both the adapter and the implementation. That is what lets either side be replaced without rewriting the other.
Runnable proof: the same module in two applications
The checkout contains a small example that makes reuse concrete:
notes-module/contractNote, CreateNote, Notes, NoteStore, NotesServiceapps/api/adapterAxum adapter: POST /notes, GET /notes/{id}apps/report/adapterCommand-line app with no web dependencyThe module crate depends only on iet-core and Serde. The API app adds Axum and
iet-http-axum; the report app adds only Tokio.
Run the command-line application:
note 1: Prepared from a command-line job
It calls the capability directly:
// examples/reusable-notes/apps/report/src/main.rs
let notes = new;
let created = notes.create.await?;
Start the API in another terminal:
HTTP/1.1 201 Created
{"id":1,"text":"same module, web caller"}
Its handler is an adapter over the same capability:
// examples/reusable-notes/apps/api/src/main.rs
async
Neither entry point duplicates the validation or storage rule. The API adds HTTP parsing and response mapping; the report app adds none of that.
Why this tends to produce fast code
The core contracts use static dispatch. Repository and service methods return
impl Future + Send rather than boxed trait objects, and adapters are generic over
the ports. The compiler sees the concrete service type and can inline across the
boundary.
The framework also avoids:
- a runtime service locator or global container;
- reflective module discovery;
- a dynamic map lookup to find the service for each request;
- mandatory boxing at every port.
These are structural advantages, not a guarantee. Database round trips, allocations, serialization, middleware, and deployment dominate real throughput. Measure the whole application under its workload. An individual feature can still introduce its own costs.
Why the code stays readable
There is no annotation that hides the wiring and no registry to inspect at runtime. A constructor call and a type signature show what will run:
let tasks = new;
let router = api.module?;
The compiler checks that the adapter's T: Tasks bound is satisfied. If you swap
implementations, incompatible wiring fails at build time rather than at the first
request.
That is also the trade-off: adding or removing a module changes the Cargo graph and requires a rebuild. This framework chooses explicit, compile-time composition over dynamic plugins.
Start small, split when reuse is real
You do not need four crates for a first application. Start with one crate and keep the boundaries clear:
task.rscontractTypes, rules, and portsservice.rsimplementationApplication behaviormemory.rsimplementationStorage mechanicsroutes.rsadapterHTTP translationPromote a boundary to its own crate when another application, another adapter, or another implementation actually needs it. A module becomes reusable when its dependencies stop pointing outward—not merely when its files are separated.
What modules do not do automatically
- Ownership is not inferred. A
Principalparameter does not filter rows; the repository must include the owner in its predicates. - Transactions are not implicit. Use
CallScope/TransactionScopewhen several database operations must succeed together. - Authentication is not installed. An adapter must obtain a trusted principal before calling a protected operation.
- Performance is not guaranteed. Static wiring removes some overhead; it does not optimize your queries.
Next: choose the capabilities your application needs, read the framework architecture, or follow the notes application to see the same boundaries in a small runnable example.