Learn / Rust Practical Systems
Ownership in Services
Use ownership and shared state deliberately so handlers stay simple and safe.
Course: Rust Practical Systems. Level: Intermediate. Topic: Systems reliability.
Stage: basic - Basic service state - Ownership in Axum service state. Use shared state, typed handlers, and response DTOs without fighting ownership.
Outcomes
- Choose Arc for shared immutable state.
- Keep handler signatures readable.
- Serialize response types cleanly.
- Describe the shape of a Rust web service built with Axum or Actix Web
- Use serde for typed JSON request and response data
- Model shared application state and error responses explicitly
Concepts
- ownership
- Arc
- state extractor
- serde
- Web APIs
- Typed handlers
- State and errors
- Rust for Reliable Systems and Services foundations
- Guided practice
Concept flow
Show how ownership in axum service state moves from trigger to implementation outcome in Rust Systems.
- Router
- State
- Handler
- Response DTO
- Client
Session flow
- Model ownership (concept, 9 min) — Name the decisions behind ownership before writing code.
- Choose Arc for shared immutable state.
- Explain where ownership belongs in job orchestration service.
- Build the vertical slice (walkthrough, 17 min) — Implement the smallest useful slice in src/http.rs.
- Keep handler signatures readable.
- Connect Arc to a working example.
- Verify and harden (exercise, 11 min) — Write a handler test.
- Serialize response types cleanly.
- Record one risk or follow-up before moving on.
- Building JSON APIs: Typed handlers (concept, 60 min) — Modern Rust web frameworks use typed extractors for path parameters, query strings, JSON bodies, headers, and shared state. Handlers return types that can be converted into HTTP responses.
- Retained source example: Axum JSON handler
use axum::{extract::Path, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateTodo {
title: String,
}
#[derive(Serialize)]
struct Todo {
id: u64,
title: String,
completed: bool,
}
async fn create_todo(Path(id): Path<u64>, Json(input): Json<CreateTodo>) -> Json<Todo> {
Json(Todo { id, title: input.title, completed: false })
}
The function signature describes which parts of the request the handler needs.
- Building JSON APIs: State and errors (walkthrough, 60 min) — Application state is commonly wrapped in Arc and injected through framework state extractors. Errors should map to status codes and JSON bodies in one place so handlers stay focused on use cases.
- Retained source example: Structured API error concept
use axum::http::StatusCode;
use serde::Serialize;
#[derive(Serialize)]
struct ErrorBody {
code: &'static str,
message: String,
}
fn not_found(resource: &str) -> (StatusCode, axum::Json<ErrorBody>) {
(
StatusCode::NOT_FOUND,
axum::Json(ErrorBody {
code: "not_found",
message: format!("{resource} was not found"),
}),
)
}
Centralized response shapes make clients and tests more predictable.
- Building JSON APIs: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Keep handlers thin: extract, validate, call a domain service, return a typed response.
- Practice: Create stable error response shapes before client integrations depend on them.
- Practice: Use tracing spans and request IDs for production observability.
- Avoid: Putting all business logic directly in route handlers.
- Avoid: Returning inconsistent ad hoc error formats.
- Avoid: Sharing mutable state without clear synchronization and ownership boundaries.
- Building JSON APIs: references (review, 2 min) — Original references retained from the legacy library.
- Axum: https://docs.rs/axum/latest/axum/
- Actix Web: https://actix.rs/
- serde: https://serde.rs/
Code example
Rust in src/http.rs.
use axum::{extract::State, routing::get, Json, Router};
use serde::Serialize;
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
version: Arc<str>,
}
#[derive(Serialize)]
struct Health {
ok: bool,
version: String,
}
async fn health(State(state): State<AppState>) -> Json<Health> {
Json(Health {
ok: true,
version: state.version.to_string(),
})
}
fn app(state: AppState) -> Router {
Router::new().route("/health", get(health)).with_state(state)
}
Walkthrough examples
- Ownership in Services in a job orchestration service — A team is extending an Axum service with a companion CLI and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: src/http.rs
- File: tests/ownership-in-services.spec
- File: docs/rust-systems/ownership-in-services.md
- Start from the provided Rust snippet and make the intent visible in names and boundaries.
- Apply the checklist item "Define state type" before adding extra behavior.
- Write down how the implementation changes when Arc fails or becomes slow.
- Axum JSON handler — The function signature describes which parts of the request the handler needs.
- Retained source code:
use axum::{extract::Path, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateTodo {
title: String,
}
#[derive(Serialize)]
struct Todo {
id: u64,
title: String,
completed: bool,
}
async fn create_todo(Path(id): Path<u64>, Json(input): Json<CreateTodo>) -> Json<Todo> {
Json(Todo { id, title: input.title, completed: false })
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Structured API error concept — Centralized response shapes make clients and tests more predictable.
- Retained source code:
use axum::http::StatusCode;
use serde::Serialize;
#[derive(Serialize)]
struct ErrorBody {
code: &'static str,
message: String,
}
fn not_found(resource: &str) -> (StatusCode, axum::Json<ErrorBody>) {
(
StatusCode::NOT_FOUND,
axum::Json(ErrorBody {
code: "not_found",
message: format!("{resource} was not found"),
}),
)
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Typical API dependencies — A small API usually combines a web framework, serialization, runtime, and structured logging.
- Retained source code:
[dependencies]
axum = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tracing = "0.1"
tracing-subscriber = "0.3"
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Add one shared config value to state.
- Return a typed JSON response.
- Write a handler test.
- Map request data to extractors: For path id, JSON body, and shared state, name the common Axum extractor. The guided runner checks extractor names only.
- Starter code: path id:
json body:
shared state:
- Hint: Path extracts route parameters.
- Hint: Json extracts JSON bodies.
- Hint: State extracts application state.
- Reference solution: path id: Path
json body: Json
shared state: State
- Accepted answers: Path | Json | State
Checklist
- Define state type
- Attach router state
- Serialize DTO
- Test handler
Quiz prompts
- Why wrap shared read-mostly state in Arc? — Arc gives multiple handlers shared ownership of immutable or internally synchronized state.
- A teammate wants to hide ownership inside a convenient helper. What should you check first? — Place ownership at the boundary that keeps job orchestration service behavior explicit, testable, and reviewable.
- Which artifact best proves this Rust Systems lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Basic service state: a teammate says the happy path works, but "Shared Arc state" is still implicit. What should you ask for before merging? — Shared Arc state belongs in the basic stage only when the decision is visible, testable, and tied to a realistic failure mode.
- A reviewer has five minutes to evaluate this basic Rust Systems slice. Which evidence is strongest? — Create the basic Axum state slice: shared typed state, clean handler signature, stable DTO, and a handler test.
- Why is serde central to many Rust web APIs? — serde derives let Rust structs map cleanly to request and response formats.
Flashcards
- Basic service state: what decision does "Shared Arc state" force you to make? Share read-mostly application state across async handlers cheaply and safely. Evidence prompt: Add one shared config value to Axum state and explain why cloning is cheap.
- Basic service state: what decision does "Handler signature" force you to make? Keep extractor and return types readable so ownership rules are obvious to reviewers. Evidence prompt: Simplify one handler signature and move transformation code into a helper.
- Basic service state: what decision does "Serde boundary" force you to make? Separate internal types from API DTOs so serialization remains a stable contract. Evidence prompt: Create one response DTO and test its serialized shape.
- In Rust Systems, what should you remember about ownership? ownership matters here because it supports "Choose Arc for shared immutable state.".
- In Rust Systems, what should you remember about Arc? Arc matters here because it supports "Keep handler signatures readable.".
- In Rust Systems, what should you remember about state extractor? state extractor matters here because it supports "Serialize response types cleanly.".
- In Rust Systems, what should you remember about serde? serde matters here because it supports "Choose Arc for shared immutable state.".
Labs
- Ship a ownership in services slice — Extend an Axum service with a companion CLI with a small but reviewable feature that proves the lesson's architecture in code.
- Create the basic Axum state slice: shared typed state, clean handler signature, stable DTO, and a handler test.
- Add one shared config value to Axum state and explain why cloning is cheap.
- Simplify one handler signature and move transformation code into a helper.
- Create one response DTO and test its serialized shape.
- Add one shared config value to state.
- Return a typed JSON response.
- The lab demonstrates the basic service state outcome without skipping earlier contract evidence.
- Each subtopic has a concrete artifact: Shared Arc state, Handler signature, Serde boundary.
- The review notes explain how this level changes ownership, verification, or operational risk.
Challenge
- Review-ready ownership in services (Core) — Create the basic Axum state slice: shared typed state, clean handler signature, stable DTO, and a handler test.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from src/http.rs plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL