A crate boundary is easier to test when application code depends on a trait instead of a concrete external client.

Program

Play the program to choose a retry count and see the client wrapped behind a small trait.

retries
dependency_adapter.rs
Replay: real traced execution (multi-file project)
fn main() {
    let retries = 2;
    let client = MockClient { status: "ok" };
    let message = fetch_status(&client, retries);
    println!("{message}");
}

trait StatusClient {
    fn status(&self) -> &'static str;
}

struct MockClient {
    status: &'static str,
}

impl StatusClient for MockClient {
    fn status(&self) -> &'static str {
        self.status
    }
}

fn fetch_status(client: &impl StatusClient, retries: i32) -> String {
    format!("{} after {} retries", client.status(), retries)
}
fn main() {
    let retries = 0;
    let client = MockClient { status: "ok" };
    let message = fetch_status(&client, retries);
    println!("{message}");
}

trait StatusClient {
    fn status(&self) -> &'static str;
}

struct MockClient {
    status: &'static str,
}

impl StatusClient for MockClient {
    fn status(&self) -> &'static str {
        self.status
    }
}

fn fetch_status(client: &impl StatusClient, retries: i32) -> String {
    format!("{} after {} retries", client.status(), retries)
}
fn main() {
    let retries = 4;
    let client = MockClient { status: "ok" };
    let message = fetch_status(&client, retries);
    println!("{message}");
}

trait StatusClient {
    fn status(&self) -> &'static str;
}

struct MockClient {
    status: &'static str,
}

impl StatusClient for MockClient {
    fn status(&self) -> &'static str {
        self.status
    }
}

fn fetch_status(client: &impl StatusClient, retries: i32) -> String {
    format!("{} after {} retries", client.status(), retries)
}
  1. retries ← 2, client ← (empty)

    1fn main() {2    let retrie→ 2s = 2; //@retries=2, 0, 43    let clien→ (empty)t = MockClient { status: "ok" };4    let message = fetch_status(&clien(empty)t, retrie2s);5    println!("{message}");
  2. message ← "ok after 2 retries"

    3    let client = MockClient { status: "ok" };4    let messag→ "ok after 2 retries"e = fetch_status(&clien(empty)t, retrie2s);5    println!("{message}");6}
    outputok after 2 retries
  1. retries ← 0, client ← (empty)

    1fn main() {2    let retrie→ 0s = 0;3    let clien→ (empty)t = MockClient { status: "ok" };4    let message = fetch_status(&clien(empty)t, retrie0s);5    println!("{message}");
  2. message ← "ok after 0 retries"

    3    let client = MockClient { status: "ok" };4    let messag→ "ok after 0 retries"e = fetch_status(&clien(empty)t, retrie0s);5    println!("{message}");6}
    outputok after 0 retries
  1. retries ← 4, client ← (empty)

    1fn main() {2    let retrie→ 4s = 4;3    let clien→ (empty)t = MockClient { status: "ok" };4    let message = fetch_status(&clien(empty)t, retrie4s);5    println!("{message}");
  2. message ← "ok after 4 retries"

    3    let client = MockClient { status: "ok" };4    let messag→ "ok after 4 retries"e = fetch_status(&clien(empty)t, retrie4s);5    println!("{message}");6}
    outputok after 4 retries
adapter `StatusClient` is the stable boundary that application code depends on.
mock `MockClient` lets the example run without a network client or third-party crate.
dependency seam `fetch_status` can accept another client later if it implements the same trait.