How to Use Tower for Async Service Composition

Tower is a modular toolkit for building robust, reusable async services in Rust, centered around the `Service` and `Layer` traits to compose functionality like logging, timeouts, and metrics.

When wrapping functions gets messy

You're writing an async handler for a chat server. It works. You send a message, the server echoes it back. Then your team asks for request logging. You add a print statement before the echo. Then they want a timeout so slow clients don't hang the connection. You wrap the future in a timeout combinator. Then metrics. Then rate limiting. Your handler is now a tangled mess of boilerplate, and the actual business logic is buried under five levels of indentation. You need a way to stack these cross-cutting concerns cleanly, so you can swap them out or reorder them without rewriting the core service. That's what Tower gives you.

Service and Layer: the building blocks

Tower revolves around two traits: Service and Layer. A Service is a black box that accepts a request and returns a future resolving to a response or an error. It doesn't care if the request is an HTTP call, a database query, or a math problem. A Layer is a wrapper factory. You pass a service into a layer, and it hands back a new service that adds behavior around the original one. The inner service stays untouched.

Think of a Service as a vending machine. You put in a request, you get a response. A Layer is like wrapping that vending machine in a box that counts how many times you use it, or checks if you have enough coins before letting you press the button. The core machine doesn't know about the box. The box doesn't know what the machine sells. You can stack boxes. The paper goes on, then the ribbon, then the bow. Each layer does its job, passes the request inward, waits for the response, and handles the result on the way out.

Stack layers. Keep the core pure.

Minimal composition

Tower provides ServiceBuilder to chain layers without writing boilerplate. You define the core logic, then wrap it with layers. The builder reads top-to-bottom, even though the layers wrap inside-out.

use std::time::Duration;
use tower::{service_fn, ServiceBuilder, ServiceExt};

#[tokio::main]
async fn main() {
    // Define the core logic as a closure service.
    // service_fn converts an async closure into a Service.
    let greeter = service_fn(|name: String| async move {
        Ok::<_, std::convert::Infallible>(format!("Hello, {}!", name))
    });

    // Stack layers around the service.
    // The timeout layer wraps the greeter.
    let mut service = ServiceBuilder::new()
        .timeout(Duration::from_secs(5))
        .service(greeter);

    // Call the service.
    // ready() checks if the service can accept a request.
    let response = service
        .ready()
        .await
        .unwrap()
        .call("World".to_string())
        .await
        .unwrap();

    println!("{}", response);
}

The greeter is a stateless service. It always returns Ok. The timeout layer wraps it. If the greeter takes longer than five seconds, the timeout layer aborts the future and returns an error. The greeter never sees the timeout. The timeout never sees the greeting string. They compose without coupling.

Convention aside: The community prefers service_fn for simple handlers. It avoids implementing the Service trait manually. Use ServiceBuilder for composition because it reads naturally. Don't chain .layer() calls manually unless you need runtime configuration.

Stack layers. Keep the core pure.

How the request flows

When you call service.ready().await, Tower checks if the service can handle a request right now. For stateless services like service_fn, this is instant. The ready future resolves immediately. Then call sends the request into the outermost layer.

The timeout layer captures the request, starts a timer, and passes the request down to the greeter. The greeter returns a future. The timeout layer awaits that future. If the future resolves before five seconds, the response bubbles back up. If the timer fires first, the timeout layer aborts the future and returns an error.

The Service trait requires a Future associated type. This future must be Send if you want to use the service across threads. The future type is often a unique anonymous type generated by an async block. Tower handles this by boxing the future or using type erasure in utilities like BoxService. The trait contract ensures that every service returns a future that can be polled. The caller awaits the future. The service does work without blocking the thread.

The future owns its state. Pin it, or the compiler will stop you.

Realistic custom layer

Built-in layers cover common cases. Sometimes you need custom behavior. A Layer is a struct that implements the Layer trait. The trait has one method: layer. It takes an inner service and returns a wrapper service. The wrapper service implements Service and delegates to the inner service while adding logic.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::{Layer, Service, ServiceExt};
use tower_layer::Layer;

// Custom layer struct.
// Layers are usually stateless, so they don't hold data.
struct LoggingLayer;

// Implement Layer to wrap a service.
// S is the inner service type.
impl<S> Layer<S> for LoggingLayer {
    type Service = LoggingService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        // Return the wrapper service holding the inner service.
        LoggingService { inner }
    }
}

// The wrapper service.
// It holds the inner service and adds logging.
struct LoggingService<S> {
    inner: S,
}

// Implement Service for the wrapper.
// It must work for any request type Req.
impl<S, Req> Service<Req> for LoggingService<S>
where
    S: Service<Req>,
{
    type Response = S::Response;
    type Error = S::Error;
    // The future must be Send for thread safety.
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // Delegate readiness to the inner service.
        // The wrapper is ready if the inner service is ready.
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Req) -> Self::Future {
        // Log the request before passing it down.
        println!("Request received");
        
        // Call the inner service.
        let fut = self.inner.call(req);
        
        // Box the future to erase the type.
        // The async block generates a unique type.
        Box::pin(async move {
            // Await the inner service's future.
            let result = fut.await;
            // Log the result after the inner service completes.
            println!("Request completed");
            result
        })
    }
}

The LoggingService implements Service for any request type Req. The bound S: Service<Req> ensures the inner service can handle the request. The wrapper delegates poll_ready to the inner service. It logs before and after the call. The call method returns a boxed future. The future awaits the inner future, logs the result, and returns it.

Convention aside: Keep unsafe blocks out of layers. Tower is about safe composition. If you need raw performance, isolate it in the core service, not the layer. Also, use tracing instead of println in production. The community expects structured logs.

Layers are generic. Write one, reuse it everywhere.

Pitfalls and compiler errors

Tower types can get verbose. If you try to store a layered service in a struct field, the type signature might span fifty lines. The compiler will reject this with E0277 if you miss a Send or Sync bound. Wrap the service in tower::util::BoxService to erase the type if you need to store it. This trades a small allocation for type simplicity.

Another common issue is backpressure. Some services have a limit on concurrent requests. The Service trait contract requires callers to await ready() before calling call(). If you skip ready, you might hit a panic or a rejected request. The compiler won't catch this. It's a runtime contract. Always use ServiceExt::ready() unless you know the service is always ready.

Futures in Tower must be Send. If your future captures a non-Send reference, the compiler rejects it. This happens when you borrow data that doesn't live long enough or isn't thread-safe. Use Arc to share data across threads. Or ensure the future doesn't escape the thread.

Check readiness. Respect the contract.

When to use what

Use service_fn when your handler is a simple async closure and you don't need stateful readiness checks.

Use ServiceBuilder when you need to stack multiple layers like logging, timeouts, and metrics around a core service.

Use a custom Layer implementation when you have reusable cross-cutting logic that applies to many services.

Use BoxService when you need to store a service behind a trait object or hide the massive type signature of a layered stack.

Use Buffer when multiple async tasks need to share a single service instance that doesn't support concurrent calls.

Use ServiceExt::ready() when you must ensure a service can accept a request before calling it, especially for services with backpressure.

Pick the abstraction that matches your complexity. Don't build a custom layer for a one-off print statement.

Where to go next