The Decorator Pattern in Rust
24 min read
In TypeScript the decorator pattern means wrapping an object in another object that has the same interface, so the wrapper can add behavior — logging, caching, retries, buffering — without the wrapped object knowing. (This is the design pattern, not the TypeScript @decorator syntax, which is a different thing entirely — more on that below.) Rust supports the exact same idea: a wrapper type that holds an inner value and implements the same trait, forwarding each method through and adding behavior around the call. The twist is that Rust gives you two ways to hold the inner value — a Box<dyn Trait> (runtime, OO-shaped) or a generic type parameter <S> (compile-time, zero-cost) — and the second is usually the idiomatic one. At the end we look at how the tower crate generalizes “a service wrapped by another service” into the reusable Layer/Service abstraction that powers axum’s middleware.
Quick Overview
Section titled “Quick Overview”A decorator is a wrapper that:
- has the same interface as the thing it wraps (so callers can’t tell the difference), and
- adds behavior around the wrapped object’s methods (before, after, or instead of forwarding).
You stack decorators to compose behavior: Retry(Cache(HttpClient)) is a client that retries, and each retry checks the cache first, and a cache miss hits the real network. Each layer is independent and reusable.
In Rust this is “a type that holds an inner value and implements the same trait.” There are three encodings to know:
- Trait-object decorator (
inner: Box<dyn Trait>) — runtime composition, the closest match to the TypeScript shape; layers chosen from config, heterogeneous lists. - Generic decorator (
inner: SwhereS: Trait) — compile-time composition, monomorphized to zero-cost static dispatch; the idiomatic default. - Function decorator — a function that takes a closure and returns a wrapped closure; the lightest form when “the thing” is just a function.
Note: This page is about wrapping types to add behavior. For swapping an algorithm (not wrapping it) see the sibling strategy-pattern.md; for building the wrapped values see factory-pattern.md. The
dyn-vs-generics mechanics live in Section 09: Trait Objects and Section 09: Trait Bounds.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”A classic data source that can be read and written, plus two decorators that transform the data on the way through. Each decorator implements the same DataSource interface and holds an inner: DataSource, so they nest freely.
// TypeScript - the classic OO decorator patterninterface DataSource { read(): string; write(data: string): void;}
class FileSource implements DataSource { constructor(private contents: string) {} read(): string { return this.contents; } write(data: string): void { this.contents = data; }}
// A decorator wraps an inner DataSource and forwards through it.class UppercaseDecorator implements DataSource { constructor(private inner: DataSource) {} read(): string { return this.inner.read().toUpperCase(); } write(data: string): void { this.inner.write(data.toUpperCase()); }}
class TrimDecorator implements DataSource { constructor(private inner: DataSource) {} read(): string { return this.inner.read().trim(); } write(data: string): void { this.inner.write(data.trim()); }}
// Compose: trim first, then uppercase.const source: DataSource = new UppercaseDecorator( new TrimDecorator(new FileSource(" hello world ")),);
console.log(source.read()); // "HELLO WORLD"source.write(" changed ");console.log(source.read()); // "CHANGED"Key properties of the TypeScript version: the decorator and the base share interface DataSource; the decorator stores inner: DataSource (a reference to anything implementing the interface); and you compose by passing one into another’s constructor.
Warning: Do not confuse this design pattern with TypeScript’s
@decoratorsyntax (@Component,@Injectable). Those are annotations applied to classes/methods — a metaprogramming feature, the rough analog of which in Rust is a macro (Section 14: Macros), not the wrapping pattern on this page. The Rust equivalent of this pattern is wrapping a value and re-implementing its trait.
Rust Equivalent
Section titled “Rust Equivalent”There are two idiomatic encodings. Start with the trait-object version because it maps one-to-one onto the TypeScript above, then see the generic version that Rust usually prefers.
Version 1: trait-object decorator (the OO shape)
Section titled “Version 1: trait-object decorator (the OO shape)”The inner value is a Box<dyn DataSource> — exactly like TypeScript’s inner: DataSource, a pointer to anything implementing the trait, chosen at runtime.
trait DataSource { fn read(&self) -> String; fn write(&mut self, data: &str);}
struct FileSource { contents: String,}
impl DataSource for FileSource { fn read(&self) -> String { self.contents.clone() } fn write(&mut self, data: &str) { self.contents = data.to_string(); }}
// A decorator OWNS the inner source (a boxed trait object) and forwards through it.struct UppercaseDecorator { inner: Box<dyn DataSource>,}
impl DataSource for UppercaseDecorator { fn read(&self) -> String { self.inner.read().to_uppercase() } fn write(&mut self, data: &str) { self.inner.write(&data.to_uppercase()); }}
struct TrimDecorator { inner: Box<dyn DataSource>,}
impl DataSource for TrimDecorator { fn read(&self) -> String { self.inner.read().trim().to_string() } fn write(&mut self, data: &str) { self.inner.write(data.trim()); }}
fn main() { let base = FileSource { contents: " hello world ".to_string() }; // Wrap base in trim, then wrap that in uppercase. Layers compose. let mut source: Box<dyn DataSource> = Box::new(UppercaseDecorator { inner: Box::new(TrimDecorator { inner: Box::new(base) }), });
println!("{}", source.read()); source.write(" changed "); println!("{}", source.read());}Real output:
HELLO WORLDCHANGEDThis is the same behavior and the same shape as the TypeScript: each decorator implements DataSource, holds an inner: Box<dyn DataSource>, and forwards through it. Composition happens at runtime, and the concrete type is erased behind dyn.
Version 2: generic decorator (static dispatch, zero-cost)
Section titled “Version 2: generic decorator (static dispatch, zero-cost)”Make the inner type a type parameter instead of a boxed trait object. Now there is no Box, no heap allocation, and no vtable — the compiler monomorphizes each layer and can inline straight through the stack.
trait DataSource { fn read(&self) -> String;}
struct FileSource { contents: String,}
impl DataSource for FileSource { fn read(&self) -> String { self.contents.clone() }}
// The inner source is a TYPE PARAMETER, so there is no Box and no vtable.struct Uppercase<S: DataSource> { inner: S,}
impl<S: DataSource> DataSource for Uppercase<S> { fn read(&self) -> String { self.inner.read().to_uppercase() }}
struct Trim<S: DataSource> { inner: S,}
impl<S: DataSource> DataSource for Trim<S> { fn read(&self) -> String { self.inner.read().trim().to_string() }}
fn main() { let base = FileSource { contents: " hello ".to_string() }; // The full type is Uppercase<Trim<FileSource>> -- known at compile time. let source = Uppercase { inner: Trim { inner: base } }; println!("{}", source.read());
// Proof there is no boxing: the concrete type is statically known. let _the_type: Uppercase<Trim<FileSource>> = source;}Real output:
HELLOThe stacked value has the concrete type Uppercase<Trim<FileSource>>. The whole decorator chain is one statically-known type, so the optimizer treats the layers as ordinary function calls it can inline. This is the version to reach for first.
Tip: Choose the encoding by asking “do I know the layers at compile time?” If yes (the common case), use generics. If the set of decorators is open or built from config at runtime, use
Box<dyn Trait>. You can even mix them: a generic decorator wrapping aBox<dyn DataSource>base.
Detailed Explanation
Section titled “Detailed Explanation”A decorator implements the trait it wraps
Section titled “A decorator implements the trait it wraps”The defining move — in both languages — is that the wrapper has the same interface as the wrapped value. In TypeScript: class UppercaseDecorator implements DataSource. In Rust: impl DataSource for UppercaseDecorator. Because the wrapper is a DataSource, callers that expect a DataSource accept it transparently, and you can wrap a decorator in another decorator without limit.
Each method does one of three things: forward to the inner value unchanged, transform the arguments before forwarding, or transform the result after forwarding. read forwards then post-processes (.to_uppercase()); write pre-processes then forwards (&data.to_uppercase()). That before/after symmetry is the whole pattern.
Box<dyn> vs <S> — runtime vs compile-time composition
Section titled “Box<dyn> vs <S> — runtime vs compile-time composition”In the trait-object version, inner: Box<dyn DataSource> is a fat pointer (data pointer + vtable pointer). Every self.inner.read() is a virtual call dispatched through the vtable at runtime — exactly like a TypeScript method call on an interface-typed field. The concrete type is erased; you can store wildly different DataSources in the same field, and you can build the stack from a loop or config.
In the generic version, inner: S is the concrete inner value, inlined into the struct’s memory. Uppercase<Trim<FileSource>> is a single struct with the FileSource’s String nested two structs deep — no pointers, no heap, no vtable. The compiler generates a specialized read for that exact type and can inline the entire chain. The cost is that the full type is “spelled out” in your signatures, and you cannot put two different stacks in the same Vec without boxing.
This is the same static-vs-dynamic-dispatch trade-off you meet everywhere in Rust (Section 09: Trait Objects), applied to the inner field of a wrapper.
You already use this pattern — in the standard library
Section titled “You already use this pattern — in the standard library”Rust’s I/O traits are built on decoration. BufReader<R> wraps any R: Read and adds buffering, and BufReader<R> is itself a Read, so it composes. Same for BufWriter<W>, flate2’s GzEncoder<W>, and so on — each wraps a Read/Write and is one too.
use std::io::{BufReader, Read};
fn main() { // `&[u8]` implements Read. BufReader<R> wraps any Read and adds buffering; // it is itself a Read, so it composes -- the std-library decorator pattern. let data: &[u8] = b"hello, decorators"; let mut reader = BufReader::new(data); let mut out = String::new(); reader.read_to_string(&mut out).unwrap(); println!("read {} bytes: {out}", out.len());
// The wrapped type is BufReader<&[u8]> -- static, zero-cost composition. fn assert_is_read<R: Read>(_: &R) {} let again = BufReader::new(b"x".as_slice()); assert_is_read(&again);}Real output:
read 17 bytes: hello, decoratorsBufReader::new is the generic decorator pattern, verbatim: a wrapper type, parameterized by the type it wraps, implementing the same trait. When you see BufReader<File>, you are reading a decorator chain.
Decoration vs inheritance
Section titled “Decoration vs inheritance”The TypeScript extends-and-super approach to “add behavior to a method” is inheritance; decoration is the composition alternative (“favor composition over inheritance”). Rust has no inheritance at all — there is no extends, no super, no base class. So in Rust, wrapping is not one option among several; it is the mechanism for layering behavior over an existing type. That makes the decorator pattern feel less like a “pattern” in Rust and more like the default way you build things up.
Key Differences
Section titled “Key Differences”| Aspect | TypeScript decorator | Rust decorator |
|---|---|---|
| Shared interface | class W implements I | impl I for W |
| Inner field | private inner: I (always a reference) | Box<dyn I> (runtime) or S: I (compile-time) |
| Dispatch | always dynamic (interface method call) | your choice: vtable (dyn) or monomorphized (<S>) |
| Cost of a layer | a heap object + a virtual call | dyn: a pointer + virtual call; <S>: zero (inlined) |
| Type after stacking | still I (erased) | dyn: erased; <S>: full type Uppercase<Trim<...>> |
| Heterogeneous list of stacks | trivial (I[]) | needs Vec<Box<dyn I>> |
| Inheritance available? | yes (extends/super) — the alternative | no — decoration is the main tool |
@decorator syntax relation | unrelated metaprogramming feature | the analog is a macro, not this pattern |
The headline difference is that Rust lets you keep the decorator pattern’s flexibility while paying none of its runtime cost, by choosing the generic encoding. TypeScript’s interface dispatch is always virtual.
Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: storing a bare dyn Trait in a field
Section titled “Pitfall 1: storing a bare dyn Trait in a field”A TypeScript developer writes inner: DataSource and expects the Rust field to be inner: dyn DataSource. But dyn DataSource is unsized — its size isn’t known at compile time — so it can’t live inline in a struct. You must put it behind a pointer (Box<dyn DataSource>, &dyn DataSource, Rc<dyn DataSource>) or make it a generic parameter.
trait DataSource { fn read(&self) -> String;}
// does not compile (E0277): a bare `dyn` field is unsized.struct Uppercase { inner: dyn DataSource,}
fn main() { let _ = std::mem::size_of::<Uppercase>();}The real compiler error:
error[E0277]: the size for values of type `(dyn DataSource + 'static)` cannot be known at compilation time --> src/main.rs:11:33 | 11 | let _ = std::mem::size_of::<Uppercase>(); | ^^^^^^^^^ doesn't have a size known at compile-time | = help: within `Uppercase`, the trait `Sized` is not implemented for `(dyn DataSource + 'static)`note: required because it appears within the type `Uppercase` --> src/main.rs:6:8 | 6 | struct Uppercase { | ^^^^^^^^^Fix: inner: Box<dyn DataSource> for the runtime version, or struct Uppercase<S: DataSource> { inner: S } for the generic version.
Pitfall 2: reaching for impl Trait in the field type
Section titled “Pitfall 2: reaching for impl Trait in the field type”The next instinct is inner: impl DataSource. But impl Trait is only allowed in function argument and return position, never in a struct field.
trait DataSource { fn read(&self) -> String;}
// does not compile (E0562): `impl Trait` is not allowed in struct fields.struct Uppercase { inner: impl DataSource,}
fn main() {}The real compiler error:
error[E0562]: `impl Trait` is not allowed in field types --> src/main.rs:7:12 |7 | inner: impl DataSource, | ^^^^^^^^^^^^^^^ | = note: `impl Trait` is only allowed in arguments and return types of functions and methodsFix: a named generic parameter, struct Uppercase<S: DataSource> { inner: S }. That gives you the same “any inner type that implements DataSource” meaning that impl Trait looks like it should provide.
Pitfall 3: forgetting that decorators are nested types, not just nested values
Section titled “Pitfall 3: forgetting that decorators are nested types, not just nested values”With the generic encoding, every wrap changes the type. If a function returns “a decorated source,” you cannot write the return type as DataSource — you must either spell the full nested type, use impl DataSource, or box it. Trying to return two different stacks from the two arms of an if will fail to unify unless you box them into Box<dyn DataSource>. This is the same “mismatched-types from heterogeneous branches” issue covered in strategy-pattern.md; the fix is the same: box at the seam where the concrete type must be forgotten.
Pitfall 4: assuming dyn decoration is “free” like in TypeScript
Section titled “Pitfall 4: assuming dyn decoration is “free” like in TypeScript”In TypeScript every method call is already a dynamic dispatch, so a Box<dyn> decorator chain feels identical. In Rust it is not free relative to the generic version: each dyn layer is a heap allocation plus a virtual call the optimizer usually cannot inline through. On a hot path, prefer the generic encoding. Use dyn deliberately, when you need runtime flexibility — not by default.
Best Practices
Section titled “Best Practices”- Default to the generic encoding (
struct Deco<S: Trait> { inner: S }). It is zero-cost and composes by type. Reach forBox<dyn Trait>only when the layers are chosen at runtime or you need a heterogeneous collection of stacks. - Keep the trait small and focused. Decorators must implement every method, so a fat trait makes every wrapper verbose. A narrow trait (one or two methods) keeps decorators short and makes them dyn-compatible for the boxed version.
- Provide a
newconstructor (Deco::new(inner)) so callers compose withRetry::new(Cache::new(base))instead of struct-literal nesting. See factory-pattern.md. - Hold shared per-decorator state with the right cell. A caching decorator needs interior mutability behind
&self; useRefCell<T>for single-threaded andMutex<T>/RwLock<T>for shared-across-threads. See Section 10: Smart Pointers. - For middleware over a request/response service, do not hand-roll it — use
tower.Layer/Serviceis the community-standard, composable form of this pattern (next section). - Don’t confuse the pattern with
#[derive]/attribute macros. If you actually want to annotate a type and generate code, that’s a macro (Section 14: Macros), not a wrapper type.
Real-World Example: a caching + retrying HTTP client
Section titled “Real-World Example: a caching + retrying HTTP client”A production-flavored client that layers two independent concerns over a base fetcher: a cache decorator that memoizes responses, and a retry decorator that re-attempts failed fetches. Each is a generic wrapper implementing the shared Fetcher trait; the cache uses RefCell for interior mutability behind &self.
use std::cell::RefCell;use std::collections::HashMap;
// A simple synchronous "fetcher": given a URL, return a body or an error.trait Fetcher { fn fetch(&self, url: &str) -> Result<String, String>;}
// The base fetcher: pretend this hits the network.struct HttpFetcher;impl Fetcher for HttpFetcher { fn fetch(&self, url: &str) -> Result<String, String> { println!("[http] GET {url}"); Ok(format!("body of {url}")) }}
// Decorator: remember responses so repeated URLs skip the inner fetch.struct Cached<F: Fetcher> { inner: F, cache: RefCell<HashMap<String, String>>,}impl<F: Fetcher> Cached<F> { fn new(inner: F) -> Self { Cached { inner, cache: RefCell::new(HashMap::new()) } }}impl<F: Fetcher> Fetcher for Cached<F> { fn fetch(&self, url: &str) -> Result<String, String> { if let Some(hit) = self.cache.borrow().get(url) { println!("[cache] hit for {url}"); return Ok(hit.clone()); } let body = self.inner.fetch(url)?; self.cache.borrow_mut().insert(url.to_string(), body.clone()); Ok(body) }}
// Decorator: retry the inner fetch up to `attempts` times on error.struct Retry<F: Fetcher> { inner: F, attempts: u32,}impl<F: Fetcher> Fetcher for Retry<F> { fn fetch(&self, url: &str) -> Result<String, String> { let mut last = Err("never ran".to_string()); for n in 1..=self.attempts { last = self.inner.fetch(url); if last.is_ok() { return last; } println!("[retry] attempt {n} failed"); } last }}
fn main() { // Compose: retry wraps caching wraps the real HTTP fetcher. let client = Retry { inner: Cached::new(HttpFetcher), attempts: 3, };
println!("{:?}", client.fetch("/users/1")); println!("{:?}", client.fetch("/users/1")); // served from cache println!("{:?}", client.fetch("/users/2"));}Real output:
[http] GET /users/1Ok("body of /users/1")[cache] hit for /users/1Ok("body of /users/1")[http] GET /users/2Ok("body of /users/2")The second /users/1 is served from the cache without touching the HTTP layer — the cache decorator short-circuited before delegating. Each concern (caching, retry, transport) is a separate, testable, reusable type, and the whole client is the single static type Retry<Cached<HttpFetcher>> with no boxing.
How tower generalizes this: Layer and Service
Section titled “How tower generalizes this: Layer and Service”The decorator pattern shows up so often in network code — logging, timeouts, retries, rate limiting, auth, compression — that the Rust ecosystem standardized it. The tower crate defines two traits:
Service<Request>— “an async function from a request to a response,” withpoll_ready(backpressure) andcall. This is the thing being decorated.Layer<S>— a factory that wraps aServiceSand returns a new, decoratedService. This is the decorator’s constructor.
A middleware is just a Service that holds an inner Service and adds behavior around call — the generic decorator pattern, applied to async request handlers. axum, tonic, hyper, and reqwest all speak tower, so a Layer you write works across the whole stack. Here is a logging middleware and a timing middleware stacked over a base service with ServiceBuilder:
First the dependencies (resolve and compile-verify in a probe project):
[dependencies]tower = { version = "0.5.3", features = ["util"] }tokio = { version = "1.52", features = ["rt", "macros"] }Or run cargo add tower --features util and cargo add tokio --features rt,macros.
use std::convert::Infallible;use std::future::Future;use std::pin::Pin;use std::task::{Context, Poll};use std::time::Instant;use tower::{Layer, Service, ServiceBuilder, ServiceExt};
// The base service: turn a request String into a response String.#[derive(Clone)]struct Greeter;
impl Service<String> for Greeter { type Response = String; type Error = Infallible; type Future = Pin<Box<dyn Future<Output = Result<String, Infallible>> + Send>>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { Poll::Ready(Ok(())) } fn call(&mut self, req: String) -> Self::Future { Box::pin(async move { Ok(format!("Hello, {req}!")) }) }}
// Decorator 1: log each request and when the inner service finishes.#[derive(Clone)]struct Logging<S> { inner: S,}impl<S> Service<String> for Logging<S>where S: Service<String, Response = String> + Clone + Send + 'static, S::Future: Send + 'static,{ type Response = S::Response; type Error = S::Error; type Future = Pin<Box<dyn Future<Output = Result<S::Response, S::Error>> + Send>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, req: String) -> Self::Future { println!("[log] request = {req:?}"); // Clone-and-swap so the *ready* inner service is the one we call. let clone = self.inner.clone(); let mut inner = std::mem::replace(&mut self.inner, clone); Box::pin(async move { let res = inner.call(req).await; println!("[log] done"); res }) }}// The Layer is the factory for the decorator: it knows how to wrap any S.#[derive(Clone)]struct LoggingLayer;impl<S> Layer<S> for LoggingLayer { type Service = Logging<S>; fn layer(&self, inner: S) -> Logging<S> { Logging { inner } }}
// Decorator 2: time how long the inner service took.#[derive(Clone)]struct Timing<S> { inner: S,}impl<S> Service<String> for Timing<S>where S: Service<String, Response = String> + Clone + Send + 'static, S::Future: Send + 'static,{ type Response = S::Response; type Error = S::Error; type Future = Pin<Box<dyn Future<Output = Result<S::Response, S::Error>> + Send>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, req: String) -> Self::Future { let clone = self.inner.clone(); let mut inner = std::mem::replace(&mut self.inner, clone); Box::pin(async move { let start = Instant::now(); let res = inner.call(req).await; let _elapsed = start.elapsed(); println!("[time] inner service finished"); res }) }}#[derive(Clone)]struct TimingLayer;impl<S> Layer<S> for TimingLayer { type Service = Timing<S>; fn layer(&self, inner: S) -> Timing<S> { Timing { inner } }}
#[tokio::main(flavor = "current_thread")]async fn main() { // ServiceBuilder stacks layers outside-in: Logging wraps Timing wraps Greeter. let mut service = ServiceBuilder::new() .layer(LoggingLayer) .layer(TimingLayer) .service(Greeter);
let response = service .ready() .await .unwrap() .call("world".to_string()) .await .unwrap();
println!("final = {response}");}Real output:
[log] request = "world"[time] inner service finished[log] donefinal = Hello, world!The structure is identical to the synchronous Cached/Retry decorators: each middleware is struct M<S> { inner: S } that implements the same trait (Service) and forwards through inner while adding behavior. The two new ideas tower adds are (1) the Layer trait, which packages “how to wrap” so a ServiceBuilder can stack decorators declaratively (outer-to-inner), and (2) the poll_ready/Future machinery for async backpressure. The .layer(LoggingLayer).layer(TimingLayer) call reads top-down as the order requests flow through — Logging sees the request first and the response last, just like Retry(Cache(...)).
Note: The clone-and-
mem::replacedance incallis the standardtoweridiom: aServicemay only becalled afterpoll_readyreturnsReady, and the readiness applies to this service instance, so middleware clones the inner service to keep a ready copy for the spawned future. In anaxumapp you almost never write this by hand — you use the ready-made layers fromtower-http(TraceLayer,TimeoutLayer,CompressionLayer,CorsLayer, …), which are exactly these decorators, written once. See Section 16: Web APIs and Section 23: Ecosystem.
Further Reading
Section titled “Further Reading”Official documentation
Section titled “Official documentation”- The Rust Book — Trait Objects for Values of Different Types — the
Box<dyn Trait>decorator encoding - The Rust Book — Generic Data Types — the
struct Deco<S>decorator encoding std::io::BufReader— the canonical decorator in the standard librarytower::Serviceandtower::Layer— the generalized middleware abstractiontower-http— ready-madeServicedecorators (tracing, timeout, compression, CORS)
Related topics in this guide
Section titled “Related topics in this guide”- Section 22 overview — the full map of common patterns
- strategy-pattern.md — swapping an algorithm vs wrapping one; the dispatch trade-offs in depth
- factory-pattern.md —
newconstructors and factories that build the wrapped values - newtype.md — a related “wrapper type” pattern, but for type safety rather than added behavior
- raii-pattern.md — wrapper types whose added behavior runs on
Drop - Section 09: Trait Objects —
Box<dyn Trait>mechanics and dynamic dispatch - Section 09: Trait Bounds —
<S: Trait>static dispatch and monomorphization - Section 10: Smart Pointers —
Box,Rc, andRefCellfor inner state - Section 14: Macros — the real Rust analog of TypeScript’s
@decoratorsyntax - Section 16: Web APIs —
tower/axummiddleware in practice - Section 23: Ecosystem —
tower,tower-http, and the middleware ecosystem - Foundations: Getting Started and Basics
Exercises
Section titled “Exercises”Exercise 1: stacked notifiers
Section titled “Exercise 1: stacked notifiers”Difficulty: Beginner
Objective: Build the basic decorator shape — a wrapper that implements the same trait as the thing it wraps.
Instructions: Define a trait Notifier { fn send(&self, msg: &str) -> String; } and a base struct Base whose send returns format!("email: {msg}"). Write a generic decorator Urgent<N: Notifier> that prepends "[URGENT] " to the message before delegating, and a generic decorator AlsoSlack<N: Notifier> that calls the inner notifier and then appends "; slack: {msg}" to the result. Compose AlsoSlack { inner: Urgent { inner: Base } } and send "disk full".
Solution
trait Notifier { fn send(&self, msg: &str) -> String;}
struct Base;impl Notifier for Base { fn send(&self, msg: &str) -> String { format!("email: {msg}") }}
// Prepend a tag, then delegate.struct Urgent<N: Notifier> { inner: N,}impl<N: Notifier> Notifier for Urgent<N> { fn send(&self, msg: &str) -> String { self.inner.send(&format!("[URGENT] {msg}")) }}
// Delegate, then add a Slack copy of the message it was handed.struct AlsoSlack<N: Notifier> { inner: N,}impl<N: Notifier> Notifier for AlsoSlack<N> { fn send(&self, msg: &str) -> String { let primary = self.inner.send(msg); format!("{primary}; slack: {msg}") }}
fn main() { let n = AlsoSlack { inner: Urgent { inner: Base } }; println!("{}", n.send("disk full"));}Real output:
email: [URGENT] disk full; slack: disk fullNote how each layer sees the message at its point in the chain: AlsoSlack forwards the raw "disk full", which Urgent tags before it reaches Base, while the Slack copy uses the untagged message AlsoSlack was handed.
Exercise 2: a counting decorator with interior mutability
Section titled “Exercise 2: a counting decorator with interior mutability”Difficulty: Intermediate
Objective: Add per-decorator state that mutates behind a shared &self, the way a real cache or metrics layer does.
Instructions: Define trait Source { fn value(&self) -> i64; } and a base struct Const(i64). Write a generic decorator Counting<S: Source> that counts how many times value is called and still returns the inner value. Because value takes &self, you cannot use a plain u32 field — use std::cell::Cell<u32>. Give it a Counting::new(inner) constructor. Call value three times and print the final value and call count.
Solution
use std::cell::Cell;
trait Source { fn value(&self) -> i64;}
struct Const(i64);impl Source for Const { fn value(&self) -> i64 { self.0 }}
struct Counting<S: Source> { inner: S, calls: Cell<u32>, // interior mutability: mutate through &self}impl<S: Source> Counting<S> { fn new(inner: S) -> Self { Counting { inner, calls: Cell::new(0) } }}impl<S: Source> Source for Counting<S> { fn value(&self) -> i64 { self.calls.set(self.calls.get() + 1); self.inner.value() }}
fn main() { let s = Counting::new(Const(42)); s.value(); s.value(); println!("value={}, calls={}", s.value(), s.calls.get());}Real output:
value=42, calls=3Cell<u32> lets the decorator track state while keeping the &self signature that the trait requires — the same trick a caching decorator uses (it just stores a RefCell<HashMap<...>> instead). For a thread-safe version you would reach for AtomicU32 or Mutex<u32>.
Exercise 3: a function decorator
Section titled “Exercise 3: a function decorator”Difficulty: Intermediate
Objective: Apply the decorator idea to a function instead of an object — the lightest form of the pattern, and the one closest to JavaScript’s higher-order functions.
Instructions: Write fn with_logging<F>(handler: F) -> impl Fn(&str) -> String where F: Fn(&str) -> String. It should return a new closure that prints the request, calls the wrapped handler, prints the result, and returns it. Decorate a handler |name| format!("Hi {name}") and call the result with "Ada".
Solution
// `with_logging` takes any handler closure and returns a wrapped one.fn with_logging<F>(handler: F) -> impl Fn(&str) -> Stringwhere F: Fn(&str) -> String,{ move |req: &str| { println!("[log] handling {req:?}"); let res = handler(req); println!("[log] -> {res:?}"); res }}
fn main() { let handler = with_logging(|name: &str| format!("Hi {name}")); println!("{}", handler("Ada"));}Real output:
[log] handling "Ada"[log] -> "Hi Ada"Hi AdaThis is the same shape as a JavaScript withLogging(fn) that returns a wrapping function — the wrapper closure moves the inner handler in and runs behavior around it. When “the thing being decorated” is just a function, this is far lighter than a trait and a wrapper struct. (tower’s Service is the async, backpressure-aware generalization of exactly this.)