Patterns & Ecosystem#

Rust has a rich ecosystem of crates and a strong culture of idiomatic patterns. This section covers the design patterns you will encounter in production Rust code and the key crates that form the practical foundation of the language.


The Builder Pattern#

For complex struct construction:

#[derive(Debug)]
struct Request {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
    body: Option<String>,
    timeout: std::time::Duration,
}

struct RequestBuilder {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
    body: Option<String>,
    timeout: std::time::Duration,
}

impl RequestBuilder {
    fn new(url: impl Into<String>) -> Self {
        Self {
            url: url.into(),
            method: "GET".to_string(),
            headers: Vec::new(),
            body: None,
            timeout: std::time::Duration::from_secs(30),
        }
    }

    fn method(mut self, method: impl Into<String>) -> Self {
        self.method = method.into();
        self
    }

    fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.push((key.into(), value.into()));
        self
    }

    fn body(mut self, body: impl Into<String>) -> Self {
        self.body = Some(body.into());
        self
    }

    fn timeout(mut self, timeout: std::time::Duration) -> Self {
        self.timeout = timeout;
        self
    }

    fn build(self) -> Request {
        Request {
            url: self.url,
            method: self.method,
            headers: self.headers,
            body: self.body,
            timeout: self.timeout,
        }
    }
}

// Usage
let req = RequestBuilder::new("https://api.example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .body(r#"{"key": "value"}"#)
    .build();

The Newtype Pattern#

Wrap a type for type safety, trait implementations, or semantic meaning:

struct UserId(u64);
struct OrderId(u64);

// These are different types — cannot be mixed
fn get_user(id: UserId) -> User { ... }
fn get_order(id: OrderId) -> Order { ... }

// get_user(OrderId(42));  // compile error!

Deriving Traits for Newtypes#

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);

impl Display for UserId {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "user:{}", self.0)
    }
}

The Typestate Pattern#

Use the type system to enforce state transitions at compile time:

struct Draft;
struct Published;

struct Article<State> {
    title: String,
    content: String,
    _state: std::marker::PhantomData<State>,
}

impl Article<Draft> {
    fn new(title: String) -> Self {
        Article {
            title,
            content: String::new(),
            _state: std::marker::PhantomData,
        }
    }

    fn edit(&mut self, content: String) {
        self.content = content;
    }

    fn publish(self) -> Article<Published> {
        Article {
            title: self.title,
            content: self.content,
            _state: std::marker::PhantomData,
        }
    }
}

impl Article<Published> {
    fn url(&self) -> String {
        format!("/articles/{}", self.title.to_lowercase().replace(' ', "-"))
    }
    // Cannot call edit() — wrong state
}

let mut draft = Article::<Draft>::new("Hello".to_string());
draft.edit("content".to_string());
let published = draft.publish();
println!("{}", published.url());
// published.edit(...);  // compile error — edit is not defined for Published

The Extension Trait Pattern#

Add methods to foreign types:

trait StringExt {
    fn truncate_with_ellipsis(&self, max_len: usize) -> String;
}

impl StringExt for str {
    fn truncate_with_ellipsis(&self, max_len: usize) -> String {
        if self.len() <= max_len {
            self.to_string()
        } else {
            format!("{}…", &self[..max_len - 1])
        }
    }
}

"Hello, World!".truncate_with_ellipsis(8);  // "Hello, …"

Key Ecosystem Crates#

Serialisation#

CratePurpose
serdeSerialisation framework (derive macros for struct ↔ any format)
serde_jsonJSON
serde_yamlYAML
tomlTOML (config files)
csvCSV
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Config {
    host: String,
    port: u16,
    #[serde(default)]
    debug: bool,
    #[serde(rename = "maxRetries")]
    max_retries: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    api_key: Option<String>,
}

// JSON
let json = serde_json::to_string_pretty(&config)?;
let config: Config = serde_json::from_str(&json)?;

CLI#

CratePurpose
clapArgument parsing (derive-based or builder)
indicatifProgress bars
dialoguerInteractive prompts
coloredTerminal colours
use clap::Parser;

#[derive(Parser)]
#[command(name = "myapp", about = "Does useful things")]
struct Cli {
    /// Input file path
    #[arg(short, long)]
    input: String,

    /// Output format
    #[arg(short, long, default_value = "json")]
    format: String,

    /// Enable verbose output
    #[arg(short, long)]
    verbose: bool,
}

fn main() {
    let cli = Cli::parse();
    println!("Input: {}", cli.input);
}

Async Runtime & HTTP#

CratePurpose
tokioAsync runtime
reqwestHTTP client
axumHTTP framework (by tokio team)
actix-webHTTP framework (actor-based)
towerMiddleware/service abstractions
tonicgRPC

Database#

CratePurpose
sqlxAsync SQL (compile-time query checking)
dieselORM / query builder
sea-ormAsync ORM
rusqliteSQLite

Error Handling#

CratePurpose
anyhowApplication error handling (context, convenience)
thiserrorLibrary error types (derive Error)
eyreanyhow alternative with custom reports
color-eyrePretty error reports with colour

Logging & Tracing#

CratePurpose
tracingStructured logging and distributed tracing
tracing-subscriberConfigure tracing output
logSimpler logging facade
env_loggerConfigure log from RUST_LOG env var

Testing#

CratePurpose
proptestProperty-based testing
rstestTest fixtures and parameterised tests
mockallMocking
assert_cmdCLI integration testing
wiremockHTTP mocking
testcontainersDocker containers for tests

Project Template#

myservice/
├── Cargo.toml
├── Cargo.lock
├── .cargo/
│   └── config.toml          # cargo configuration
├── src/
│   ├── main.rs
│   ├── lib.rs
│   ├── config.rs
│   ├── error.rs
│   ├── handler/
│   │   ├── mod.rs
│   │   └── user.rs
│   ├── service/
│   │   ├── mod.rs
│   │   └── user.rs
│   └── model/
│       ├── mod.rs
│       └── user.rs
├── tests/
│   └── api_test.rs
├── Dockerfile
├── Makefile
└── README.md

Dockerfile#

FROM rust:1.78-slim AS build
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release  # cache dependencies
COPY src ./src
RUN cargo build --release

FROM gcr.io/distroless/cc-debian12
COPY --from=build /app/target/release/myservice /
USER nonroot
ENTRYPOINT ["/myservice"]

The dependency-caching trick (cargo build with a dummy main.rs before copying actual source) avoids rebuilding all dependencies on every code change.


Key Takeaways#

  • Builder pattern for complex construction, newtype for type safety, typestate for compile-time state machines.
  • serde is the universal serialisation framework. #[derive(Serialize, Deserialize)] is all you need for most formats.
  • clap with derive macros makes CLI argument parsing declarative and type-safe.
  • tokio + axum + sqlx is the modern async web stack. reqwest for HTTP client.
  • anyhow for applications, thiserror for libraries — the standard error handling pair.
  • tracing has replaced log as the standard for structured, span-aware logging.
  • Cache dependencies in Docker builds by building with a dummy source file before copying real code.