Production patterns
This is the last lesson, and it only uses tools you already have. The order is the order you meet them in a service: where code lives, how it is configured, how a request fails in JSON, how you know it is fast enough, and how it stops.
Layout
A binary crate should be thin. Domain code does not import a web framework or a database driver. Infrastructure implements ports the domain defined. The dependency arrow points inward: the API and the database adapters depend on the domain, and the domain depends on neither.
discount_cents is domain: integers, no sockets. main only calls it. That split is what the module lesson’s privacy was for.
fn discount_cents(price: u32, percent: u32) -> u32 {
price.saturating_sub(price.saturating_mul(percent) / 100)
}
fn main() {
println!("{}", discount_cents(2_000, 10));
}Config and tracing
Read configuration at startup and fail the process if it is missing or invalid. That is expect or ? at main, the policy from error handling, not a default that hides a bad deploy. Log lines are events. A span is the region around a request. Do not log secrets, tokens, or full card numbers. A health check answers whether the process should receive traffic. It is not a place to take a new dependency at request time.
| Level | Use it for |
|---|---|
| debug | Investigation. Leave it off in production until you need it. |
| info | Normal traffic |
| warn | A retry |
| error | A failure the caller sees |
Child events inherit the request id. A plain log line without a span is harder to join later.
struct AppConfig {
port: u16,
}
impl AppConfig {
fn from_env_like(port: Option<&str>) -> Result<Self, String> {
let raw = port.ok_or_else(|| String::from("PORT missing"))?;
let port = raw.parse().map_err(|_| String::from("PORT invalid"))?;
Ok(Self { port })
}
}
fn main() {
let cfg = AppConfig::from_env_like(Some("8080")).unwrap();
println!("{}", cfg.port);
}JSON errors
Serde turns a struct into JSON and back. A request body becomes a type, and invalid JSON is a Result, not a panic. Validate by parsing into a type that cannot represent the bad state, the same enum and newtype habit. The HTTP error is a small JSON object with a code and a message, not the Debug of an internal error. Idempotency means a retried request does not apply a payment twice. The handler checks a key before the domain runs.
A missing @ never becomes a user. The type is the validation.
struct CreateUser {
email: String,
}
fn parse_user(email: &str) -> Result<CreateUser, &'static str> {
if email.contains('@') {
Ok(CreateUser { email: email.to_string() })
} else {
Err("invalid email")
}
}
fn main() {
match parse_user("a@b.com") {
Ok(user) => println!("{}", user.email),
Err(err) => eprintln!("{err}"),
}
}Performance and CI
Measure before you rewrite. The levers you already know are capacity on a Vec, borrowing instead of clone, and fewer allocations in a hot loop. clippy with warnings denied, rustfmt, and cargo test are the gate. A release profile and a stated minimum compiler version belong in that same gate. count_bytes sums lengths. It does not build an intermediate vector of copies.
Format first so the diff is about behavior. Lint next. Tests last, including the ones that do not touch the clock.
fn count_bytes(chunks: &[&[u8]]) -> usize {
chunks.iter().map(|c| c.len()).sum()
}
fn main() {
let parts: [&[u8]; 2] = [b"ab", b"cdef"];
println!("{}", count_bytes(&parts));
}Shutdown
Stop accepting work, let in-flight work finish, then exit. A flag the workers load is enough to show the two phases. Timeouts, retries, and a circuit breaker sit on the client calls you already return as Result. A retry needs an idempotency key or it will double-charge. A rate limit is a bound, the same idea as a bounded channel.
New requests are refused. Workers already holding a job finish it. Drop still runs, so files and locks close.
use std::sync::atomic::{AtomicBool, Ordering};
fn main() {
let running = AtomicBool::new(true);
running.store(false, Ordering::SeqCst);
while running.load(Ordering::SeqCst) {
// worker would serve here, then notice the flag and drain
}
println!("drained");
}