The Rust Engineering Handbook / Chapter 96
Application Archetypes: CLI, Library, Service, Data Plane, and Compute
Choose Rust architecture defaults from the product's startup, compatibility, latency, lifecycle, memory, dependency, and distribution constraints.
A release portfolio contains five Rust deliverables:
repo-audit, a command-line tool that runs in developer shells and CI;ledger-format, a library used by forty downstream applications;event-gateway, a continuously running network service;packet-shaper, a latency-sensitive data plane pinned to dedicated cores;model-scan, a batch compute job processing multi-gigabyte datasets.
The platform group proposes one standard stack: an async runtime, dynamic configuration, structured tracing, a plugin interface, a large shared dependency platform, and one container image template. Each item is defensible somewhere. Applying all of them everywhere gives the CLI runtime startup it does not need, couples the library’s public behavior to an executor, adds dynamic dispatch to the packet fast path, forces the compute job into service lifecycle conventions, and gives every artifact the supply-chain surface required by the most complicated one.
Rust defaults should follow product shape. The language’s ownership and type contracts do not change, but the dominant contract does: immediate diagnostics for a CLI, downstream compatibility for a library, lifecycle and overload for a service, tail latency for a data plane, and throughput plus bounded working sets for compute.
Use four questions before choosing a framework or repository template:
- What is the unit of delivery and compatibility? A binary artifact, public crate API, rolling network protocol, appliance image, or repeatable job result?
- Where does waiting or parallel work occur? Human-paced invocations, caller-owned execution, many concurrent sockets, pinned queues, or divisible datasets?
- What failure must remain local? One command, one caller, one request, one packet flow, or one partition?
- Which budget dominates? Startup, ecosystem compatibility, service availability, p99 latency, memory ceiling, or total throughput?
The answers produce starting points, not immutable categories. Real products combine archetypes. A CLI can contain a reusable library; a service can have a data-plane core; a compute engine can expose a long-running control service. Name the dominant path and give each secondary path its own boundary.
The comparison matrix
The first matrix covers lifecycle and execution. “Default” means the first design to evaluate, not a mandate.
| Archetype | Dominant contract | Startup and exit | Execution starting point | Memory stance |
|---|---|---|---|---|
| CLI | Complete one user-visible operation with actionable diagnostics | Fast startup; explicit exit code; terminal-safe output; interruption leaves artifacts valid | Synchronous main; add bounded parallelism or async only around real waiting concurrency | Bounded by input or explicit streaming; avoid loading entire repositories by accident |
| Library | Preserve caller choice and compatible behavior | No process ownership; constructors should not start hidden global work | Synchronous or runtime-neutral core; expose async only when the operation is inherently asynchronous | Caller-visible allocation; borrowed views and caller buffers where they improve the API |
| Service | Remain operable through load, faults, upgrades, and shutdown | Validate before ready; drain on termination; bounded startup and shutdown | Async for high waiting concurrency, with isolated blocking/CPU work | Explicit queues, concurrency limits, cache ceilings, and overload behavior |
| Data plane | Preserve latency and forwarding behavior under load | Preallocate and warm; fail closed or degrade by policy; restart boundary is architectural | Pinned workers, polling, batching, or carefully selected async; minimal sharing | Fixed pools and per-core budgets; allocation-free steady state where measured need justifies it |
| Compute | Finish a reproducible workload efficiently and recoverably | Validate inputs; checkpoint or partition; return durable result and job status | Data parallelism, staged pipelines, accelerator submission, or external scheduler integration | Bounded chunks and spill policy; account for dataset expansion and intermediate state |
The second matrix covers compatibility, dependencies, observability, and distribution.
| Archetype | Compatibility center | Dependency tolerance | Observability | Distribution starting point |
|---|---|---|---|---|
| CLI | Flags, config, output schema, exit codes, and artifact behavior | Moderate if startup, size, licensing, and install reliability stay acceptable | Human diagnostics by default; structured output and optional debug logs for automation | Native binaries or package managers; target-specific artifacts with checksums and provenance |
| Library | Public Rust API, feature graph, MSRV, SemVer, and documented behavior | Low to moderate; transitive public types, build scripts, and MSRV raises have downstream cost | No unsolicited logging; expose errors, hooks, or caller-selected instrumentation | Registry/source package plus docs; validate package contents and supported target matrix |
| Service | Network/storage schemas, configuration, rollout, and rollback | Moderate to high when owned and governed; every dependency joins operations and security scope | Structured events, metrics, traces, health, readiness, and runbook signals | Container, package, or host image with migration and mixed-version policy |
| Data plane | Wire behavior, control-plane contract, device/platform support, and upgrade continuity | Low on the hot path; specialized native or unsafe dependencies require strong ownership | Low-overhead counters, histograms, sampled traces, queue and drop reasons | Appliance, package, container, or firmware image tied to hardware/kernel/runtime constraints |
| Compute | Input/output formats, numeric/reproducibility policy, checkpoint schema, and job interface | Moderate to high if performance value is measured and deployment remains reproducible | Stage timing, throughput, memory, skew, progress, checkpoint, and failure classification | Scheduler image or native bundle with data, accelerator, and platform compatibility recorded |
Read both matrices by column before reading them by row. The columns are the reusable method: identify the compatibility unit, execution shape, failure boundary, dominant budget, and operating evidence. The archetype rows are worked answers to those questions, not five unrelated templates.
The dimension most likely to reverse these defaults is a hard external constraint. A CLI that concurrently scans ten thousand remote repositories may earn async I/O. A library embedded only in one controlled service may accept a runtime-specific adapter. A data plane with millisecond budgets and kernel-managed I/O may use an async framework successfully. Record the reason and evidence rather than arguing from category labels.
CLI: optimize the human and automation boundary
A command-line tool is a short-lived process with two audiences: a person at a terminal and automation reading stable machine behavior. Its central architecture is usually a narrow effectful shell around testable operations.
Parse syntax, load configuration, validate the entire request, execute, render one diagnostic or result, and select an exit code. Separate human output from machine output. Human diagnostics need context, source locations where relevant, suggestions that preserve correctness, and terminal-aware formatting. Machine output needs a versioned schema, stable encoding, and no progress bars or incidental logs mixed into standard output.
Use standard error for diagnostics and progress, standard output for the requested result, and documented exit codes for broad outcome classes. Do not assign a unique exit code to every internal error unless automation can act on the distinction. Preserve error sources internally; render once at the process boundary, with secret and path policy applied.
Synchronous execution is an excellent default because it keeps control flow, cancellation, and startup small. Add threads for measured CPU parallelism and async for many simultaneous waits, not for one file read followed by one network request. An async CLI must decide how Ctrl-C cancels tasks, whether partial artifacts remain, and how runtime initialization affects startup and binary size.
Stream large inputs. A repository scanner can walk entries and emit bounded findings rather than building a complete in-memory model. If stable sorting requires all results, name the memory cost or spill to disk. Temporary output should use a staged write and atomic replacement where the platform supports it, so interruption preserves the old artifact.
Static linking can make a CLI easier to copy, but the phrase hides platform details. C library, DNS, certificates, plugins, system APIs, licensing, and target policy affect whether a fully static artifact is feasible or desirable. Publish target-specific builds, record linkage, and test on the oldest claimed environment. One binary compiled on a maintainer laptop is not a distribution strategy.
Plugins are rarely a free CLI feature. In-process native plugins add ABI, allocator, panic, and supply-chain contracts. Wasm plugins add an engine, capability boundary, memory copies, and versioned imports. Subprocess plugins add startup and protocol overhead but improve fault containment. Prefer declarative configuration or subprocess protocols until extension needs justify a more complex boundary.
Review a CLI by running it in four modes: interactive success, interactive failure, noninteractive structured output, and interruption during an effect. Measure cold startup when users invoke it frequently. Test paths, encodings, terminals, signals, and installation on every supported platform.
Library: the caller owns the process
A library must not silently take ownership of global lifecycle. It should not install a process-wide panic hook, configure global logging, create an immortal runtime, read ambient configuration at import time, or terminate the process. Those choices belong to the application unless the library’s narrow purpose explicitly grants that authority.
The public contract includes ownership in signatures, error stability, panic conditions, thread-safety properties, cancellation, feature behavior, platform support, MSRV, and SemVer. Downstream compile time and dependency resolution are operational costs even when the library itself runs quickly.
Keep the core runtime-neutral when practical. A parser can accept &[u8]; storage logic can use a synchronous trait for already-available data and separate adapters for blocking or async I/O; a protocol state machine can be pure. If an operation is naturally async, expose an honest async contract, including future Send properties and cancellation behavior, rather than blocking inside a nominally synchronous call.
Avoid turning every input into impl Into<String> or every adapter into a generic type parameter. Generics improve static composition but expand downstream monomorphization, diagnostics, and compatibility surface. Trait objects stabilize a dynamic boundary but add lifetime, allocation, and dispatch choices. Concrete types are often the best stable public result. Put flexibility at boundaries with demonstrated variation.
Features should be additive capabilities, not mutually exclusive product modes. Test no-default, default, important isolated features, and supported combinations. Minimize dependencies that appear in public types because replacing them can become a breaking change. A private dependency still affects MSRV, build scripts, licenses, advisories, and compile time.
Library observability is caller-controlled. Return structured errors and, when necessary, accept hooks or integrate with conventions that do not force global initialization. Never print routine diagnostics from a reusable library. If instrumentation has material overhead or dependency cost, make the trade-off visible and test enabled and disabled configurations.
Distribution means more than uploading source. Inspect the package contents, include licenses and metadata, build documentation for supported features, run doctests, and test against the declared MSRV. Decide whether to commit a lockfile for repository reproducibility even though downstream library resolution does not consume it in the same way as an application lockfile.
Service: lifecycle is part of correctness
A service is judged across time: startup, readiness, steady load, partial dependency failure, overload, configuration change, deployment, shutdown, and recovery. Correct request handlers inside an unbounded or unobservable process are not a correct service.
Startup should parse and validate configuration, establish required dependencies, construct bounded resources, and expose readiness only when the instance can accept its promised traffic. Optional dependency failure may produce a named degraded mode. A process that becomes ready before migrations, listeners, or credentials are usable creates rollout races.
Async is a strong fit for many services because they maintain large numbers of waiting operations. It is not a license to mix CPU work, blocking calls, and unbounded tasks into one executor. Give blocking work a bounded pool, give CPU-heavy work explicit parallel resources, and tie every spawned task to a service lifecycle. Admission control must occur before expensive parsing, allocation, or downstream fan-out.
Shutdown has phases: stop admission, signal child work, stop spawning, drain within a deadline, cancel or persist remainder, flush bounded telemetry, and exit with a truthful status. Dropping a future may or may not cancel an external operation. Document the actual effect and make retries idempotent.
Memory ceilings come from more than heap size: connection buffers, per-request state, queues, retries, caches, telemetry, TLS, runtime tasks, and kernel socket buffers all contribute. Derive a capacity model, set queue and concurrency bounds, and observe saturation. Overload behavior should be an intentional response—reject, shed, degrade, or redirect—not memory growth until the process is killed.
Compatibility centers on rolling systems. Network schemas need additive evolution or negotiated versions; storage migrations need mixed-version analysis; configuration changes need rollback; health semantics need deployer alignment. Static linking may simplify the runtime image, but security patching, native dependencies, certificates, DNS, and debugging tools still need an update path.
Structured telemetry is required because no person watches one invocation. Use stable event names and fields, bounded-cardinality metrics, latency distributions, correlation where it answers operational questions, and redaction at the source. Health says whether the process is alive; readiness says whether traffic should arrive. Neither replaces domain and saturation signals.
Data plane: make the hot path a separate product
A data plane processes packets, messages, storage blocks, media frames, or similarly frequent units under a tight latency and loss contract. It often has a slower control plane for configuration and telemetry. Treat the two paths separately.
The hot path may favor pinned workers, one queue per core, preallocated pools, batched system calls, explicit NUMA placement, or kernel-bypass I/O. These are hypotheses until measured on the deployment hardware and workload. Rust moves are not automatically zero-copy; reference counting is not automatically too slow; async is not automatically unsuitable. Inspect allocations, cache misses, queueing, syscalls, synchronization, and tail latency.
Ownership should follow flow. Receive ownership from a device or socket, validate once, attach bounded metadata, transform, enqueue to a named next owner, and release or transmit. Shared mutable global policy on every packet invites contention. Publish immutable snapshots or versioned handles from the control plane, with an explicit reclamation strategy.
Steady-state allocation-free operation can be a valuable goal when allocator latency or fragmentation affects tails. It requires fixed pools, a maximum in-flight count, and a behavior when exhausted. Preallocation merely moves failure to startup unless capacity can adapt safely. Record packet drops, pool exhaustion, ring occupancy, backpressure, and reason codes without doing unbounded formatting on the fast path.
Failure policy is domain-specific. Dropping one best-effort telemetry packet may be acceptable; partially applying a security policy may not be. Define fail-open, fail-closed, bypass, and degraded modes per operation. Watchdog restarts can restore liveness but must not turn a persistent overload or corrupt input into a restart storm.
Dependencies on the hot path deserve stricter review: allocation behavior, vector instruction requirements, unsafe surface, kernel/driver integration, maintenance, and reproducible performance. Keep convenience and control-plane dependencies outside the data-path crate when possible. A plugin call per packet is a major architecture decision; prefer compiled policy, closed enums, or batched sandbox calls unless dynamic extension is worth the latency and failure boundary.
Test functional behavior, saturation, tail distributions, CPU/core scaling, memory ceilings, queue fairness, device reset, and mixed configuration versions. Synthetic microbenchmarks identify mechanisms; replay and hardware tests establish system behavior.
Compute: throughput is constrained by the working set
Compute applications transform bounded or very large datasets and often run under a scheduler. The dominant objective may be time to result, cost per item, deterministic output, or accelerator utilization. A fast kernel surrounded by unbounded decoding and intermediate copies is not a fast job.
Begin with a stage graph: input, decode, transform, partition, compute, reduce, encode, and persist. For each edge, name item size, expansion ratio, concurrency, queue capacity, ownership, and spill behavior. Batch sizes should amortize overhead without exceeding cache, memory, latency, or recovery budgets.
Data parallelism is a natural starting point for independent partitions. Use deterministic reduction when output reproducibility matters; floating-point associativity means different partition orders can change low bits. Record numeric modes, CPU features, accelerator versions, libraries, seeds, and build profile with performance results.
Async may help overlap object storage or network I/O, while a bounded CPU pool performs transforms. Do not run long CPU loops on an executor intended for short cooperative tasks. Dedicated threads or a data-parallel scheduler may be clearer. Accelerator submission introduces device memory, transfer, stream, synchronization, and error recovery contracts resembling DMA at a larger scale.
Memory is a first-class input. Measure resident set, allocator behavior, mapped files, decompression expansion, per-worker scratch, output buffering, and scheduler limits. Prefer chunked input and bounded stages. When spilling is allowed, define disk capacity, cleanup, encryption, locality, and restart semantics.
Checkpointing is useful only when the checkpoint has a version, atomic publication, input identity, and replay rule. A job retry must not duplicate external effects or mix outputs from different code/data versions. Partition-level retry can reduce recovery cost but increases metadata and idempotency work.
Compute observability answers different questions than service telemetry: items and bytes per stage, skew, stalled partitions, accelerator utilization, memory high-water mark, spill volume, checkpoint age, retry classification, and estimated completion. Per-item traces can be ruinously expensive; sample and aggregate according to the debugging model.
Distribution must bind code to data and hardware assumptions. A container is useful but does not guarantee compatible GPU drivers, CPU features, filesystem semantics, or scheduler resources. Publish a compatibility manifest and refuse unsupported combinations early.
Mixed archetypes need internal borders
Most mature products are combinations. The design error is allowing the most demanding secondary shape to dictate every internal API.
A CLI with a reusable library should keep parsing and terminal behavior in the binary while the library accepts explicit inputs and returns typed results. A service with a data-plane core should isolate async sockets and lifecycle from a synchronous or pinned bounded engine. A compute platform with a service control plane should submit immutable job specifications rather than sharing executor internals. A library offering plugins should put dynamic behavior behind an optional adapter so ordinary callers do not inherit an engine.
Draw internal borders around execution and compatibility:
service shell: config -> admission -> async I/O -> shutdown
|
v owned batch
data-plane core: pinned worker -> bounded queue -> transform
The owned batch prevents a runtime-specific future, socket borrow, or service lock from leaking into the core. The core returns bounded results and saturation signals. Each layer can then use the verification and observability methods suited to its shape.
Exceptions need a decision record with four fields: default being reversed, constraint that reverses it, evidence supporting the exception, and revisit trigger. “The team already uses framework X” is a maintenance consideration, but it is not evidence that the framework belongs in a public library or packet hot path.
Five product decisions
Apply the matrix to the portfolio. Each decision includes one justified exception so the exercise does not become category matching.
1. repo-audit: remote-aware CLI
Start with a synchronous command shell and a pure scanning library. Stream filesystem entries, render human diagnostics to standard error, provide versioned JSON on standard output, use staged artifact replacement, and publish native binaries for named targets.
Exception: remote repository metadata can involve hundreds of simultaneous waits. Use an async adapter for that subcommand with a concurrency limit of 32, a command-wide deadline, Ctrl-C propagation, and no async types in the scanner library. Measure cold startup and binary size before making the runtime universal.
2. ledger-format: public library
Make borrowed byte decoding and owned validated values the core API. Keep fields private, errors typed, features additive, MSRV explicit, dependencies small, and runtime ownership with the caller. Supply doctests, compile tests, package-content checks, and a SemVer review.
Exception: downstream services need asynchronous stream decoding. Add a separate adapter crate or feature tied to a named I/O trait after proving demand. Document runtime and Send behavior. Preserve the synchronous parser as the compatibility center.
3. event-gateway: network service
Use async I/O for connections, bounded admission and queues, a dedicated blocking pool for compression, structured telemetry, explicit readiness, and phased shutdown. Package a container and support rolling protocol/schema evolution and rollback.
Exception: signature verification consumes enough CPU to stall cooperative workers. Move it to a bounded CPU pool sized from load evidence. Reject or shed before spawning verification when that pool and its queue are full.
4. packet-shaper: data plane
Use one pinned worker per receive queue, fixed packet pools, immutable policy snapshots, bounded handoffs, low-overhead counters, and explicit device/reset ownership. Keep the control plane separate and build for named kernel, driver, CPU, and NIC combinations.
Exception: a sandboxed policy plugin is required for customer extensibility. Invoke it on batches in a non-owning decision stage with strict fuel/time/memory limits and a predeclared fallback, not once per packet through an ambient-capability interface. Benchmark p50, p99, and drop behavior with and without the plugin.
5. model-scan: batch compute
Partition input, use bounded data-parallel stages, track memory expansion, checkpoint atomic partition outputs, and publish stage throughput, skew, memory, spill, and retry signals. Bind builds to CPU feature and dataset-format manifests.
Exception: object-store reads dominate and are highly concurrent. Use async only in input/output stages, passing owned bounded batches to the compute pool. Backpressure storage reads when compute or memory reaches its budget.
The fixture in examples/rust-engineering-handbook/part-15/platform-boundaries encodes a few starting defaults as data and tests that library, service, and data-plane choices differ. It does not choose architecture automatically. The valuable artifact is the justification and exception evidence.
Architecture review questions
- What is the dominant archetype, and which paths have a different shape?
- Does the unit of compatibility match what users actually install, import, call, or roll out?
- Who owns process startup, global hooks, executors, configuration, and termination?
- Is async attached to genuine waiting concurrency, and is CPU/blocking work separated?
- Are thread count, queue capacity, in-flight work, caches, scratch space, and retries bounded?
- Can diagnostics serve both humans and automation without corrupting result streams?
- Does a library preserve caller runtime and observability choices?
- Does a service expose truthful readiness, overload, cancellation, and shutdown behavior?
- Does a data plane define ownership, pool exhaustion, tail latency, and degraded modes?
- Does compute account for expansion, skew, spill, checkpoint compatibility, and reproducibility?
- Are static-linking and plugin decisions tied to named targets, threat models, and update paths?
- Does every exception name evidence and a condition that will reopen the decision?
Product shape is a compression tool for design review. It narrows the first set of sensible choices while keeping exceptions visible. Once a team can state why a CLI, library, service, data plane, or compute job needs different contracts, it can introduce Rust into an existing estate without forcing one architecture onto every component. That is the migration problem ahead: choose seams and sequences that preserve rollback, evidence, and organizational learning.
Sources and version notes
- The Rust API Guidelines provide durable review categories for public libraries. They are guidance rather than a substitute for a crate’s explicit compatibility and operational contract.
- The Cargo Book on features, SemVer compatibility, and
rust-versiondefine version-sensitive package behavior that library and application policies must track. - The Rust standard-library process documentation defines current process and exit facilities; signal, terminal, and atomic replacement behavior remains platform-specific.
- The architecture defaults are editorial recommendations, not language guarantees. Performance and distribution choices require the named workload, target, build profile, linkage, and measurement.
- The executable
examples/rust-engineering-handbook/part-15/platform-boundariesfixture uses Rust 2024, declares MSRV 1.85, and has no third-party dependencies. Its policy test was verified with installed Rust/Cargo 1.93.1; it is evidence that the example is coherent, not proof that its defaults fit a product.
Continue reading
Full table of contents