Why this, why now

On 2026-10-29, matklad (Aleksey Kladov, author of rust-analyzer, now in the TigerBeetle / Zig world) published a short essay, "Avoid Public Functions Calling Other Public Functions". It drew 87 points and 20 comments on Lobsters. The post itself is tiny, just two Zig snippets, but the comments pulled in David Chisnall (CHERI / C++), Andrew Kelley (Zig's author), people citing Rust's Vec implementation, and people citing mutex deadlocks. The discussion grew into a very practical question: where do you cut the layers of an API?

Whether you write TypeScript or Rust, you face this decision almost daily when building a library or module. So today we dig into this one rule of thumb: why it works, and when you should break it.

The problem

You are writing a public function foo, and part of what it does is already handled by another public function bar. The DRY instinct says: just call bar() from inside foo. matklad's claim is that this causes trouble later, and that you should instead extract the shared part into a private helper and have both foo and bar call it.

// BAD
pub fn foo() void {
    return bar(.{ .frobnicate = true });
}

pub fn bar(options: struct { frobnicate: bool = false }) void {
    // ...
}

// GOOD
pub fn foo() void { return helper(.frobnicate); }
pub fn bar() void { return helper(.transmogrify); }
fn helper(mode: enum { frobnicate, transmogrify }) void {
    // ...
}

The disease in the BAD version is the frobnicate parameter itself. No outside user has a reason to set it; it grew on bar's public signature only so that foo could be implemented.

How it works: why public-to-public reuse degrades

1. A public API has one job: be the best interface for outside callers

A public function's signature should be designed to read well and resist misuse at external call sites. When the same function also serves as a building block for the implementation, two demands land on one signature: "easy to use from outside" and "easy to reuse from inside". The second keeps asking for parameters that have nothing to do with the first.

2. The damage happens after the sharing

This is the point matklad stresses. When foo first calls bar, it often looks clean and needs no extra parameters. The trouble comes six months later, when you want foo to behave slightly differently. Because foo goes through bar, the only way to express that change is to add an optional parameter to bar's public signature. That is how "optional parameters users should never set" pile up. On Lobsters, bputh described living this: APIs where "everything is in one function call" end up reinventing multiple functions via parameters.

3. A private helper makes the change cheap and local

If the shared part is private from the start, tweaking the helper's signature is a cheap change confined to the module. You don't worry about public API compatibility, documentation, or users coupling to it. The rule is a structural precaution: route future pressure ahead of time to the place where change is cheap.

4. The exception: shortcut APIs follow the opposite rule

matklad names an explicit exception. For "shortcut" functions, which are just a commonly used chain of calls to other public functions, the rule flips: they must not call private functions. Once a shortcut starts using a privileged internal path, it becomes a middle layer that outside users cannot reproduce. This is the same failure the LWN Linux kernel design-patterns article (Neil Brown) called the "midlayer mistake", and matklad uses the word midlayer too. Because a shortcut is built only from public APIs, a user who needs something slightly different can copy its body and adapt it.

So it helps to split relationships between public functions into two kinds:

"Public calls public" goes wrong when these two get mixed.