matklad's 'don't let public functions call public functions' rule sparks debate
A short blog post laying out an API-design rule of thumb draws sustained discussion on Lobsters and Hacker News.
What to know
- The post proposes a narrow, specific API-design rule: extract a shared private helper rather than letting one public function call another, to keep public interfaces from accumulating internal-use-only parameters.
- The author carves out an explicit exception for 'shortcut' APIs, where a public function is deliberately a convenience chain of other public calls.
- Discussion on Lobsters (66 points, 13 comments) outpaced the Hacker News submissions, and became a genuine engineering debate rather than simple link-sharing.
- Commenters split between extending the rule's reasoning (interface stability, testing) and offering counterexamples (layered C/C++ APIs, Rust's Vec) where public-to-public calls are intentional.
The dispute Whether 'public function calling public function' is itself the problem, or merely a symptom of a broader, more situational issue about protecting a function's public contract — with layered-API advocates arguing the rule is too strict. · positions read across 24 posts and comments
The rule is sound, and the deeper issue is avoiding accidental damage to a function's public interface/contract.
-
“I feel like the better more general rule would be something like "do not damage the public interface unless absolutely necessary."”
azimux · Lobsters ↗
The rule doesn't hold universally — layered or convenience public APIs legitimately have public functions calling other public functions.
-
“I’m not sure I agree with this. I tend to build public APIs in layers. The low layer public API exposes all necessary functionality.”
david_chisnall · Lobsters ↗
The post's aside on testing ('mock your IO, not your methods') is unclear about where the I/O boundary should actually be drawn.
-
“Is mocking a MyCustomResourceStore "mocking your IO", or does "mock your IO" refer to some lower layer of the I/O stack? Where does I/O begin?”
weberc2 · Lobsters ↗
matklad Blog post authorandrewrk Lobsters commenterazimux Lobsters commenterdavid_chisnall Lobsters commenter
How it unfolded 4 developments, newest first · click a bar or a number to jump postscomments
-
4
matklad replies in the thread to clarify the testing argument
Responding to a sub-thread questioning what 'mock your IO' means, matklad elaborates that there are two useful cuts in an application: the thin waist connecting the app to the external world, and a separate cut for testing the implementation of that waist itself.
“That's a good observation, that there are many different ways to cut the interfaces such that pure business is on the one side, and side-effecting syscalls are on the other. And any cut works!”
— matklad -
I’m not sure I agree with this. I tend to build public APIs in layers. The low layer public API exposes all necessary functionality. The higher layers expose *convenient* functionality for common use cases. In C/C++, the higher-level interfaces are often header-only and designed to be inlined in the caller. They also serve as documentation: if you…
2 more of the top 3 · 15 posts in this stretch
-
OP did say it’s OK to have helper functions that call _only_ other public functions.
-
Instead of reading my comment, you should watch Gary Bernhardt's superb talk "Boundaries", which discusses this very question. https://www.destroyallsoftware.com/talks/boundaries But I'll try to do a very brief answer to only your question. Your question sounds like it assumes a code structure that looks like this ... ``` def my_procedure()…
-
-
3
Several commenters push back that the rule is not universal
david_chisnall describes building layered public APIs in C/C++ where higher-level convenience functions intentionally call lower-level public functions, and ohrv notes Rust's Vec has many public methods calling other public methods for safety and reuse reasons.
“I’m not sure I agree with this. I tend to build public APIs in layers.”
— david_chisnall -
Or you could do whatever makes the most sense at a given moment and then when the circumstances change, refactor your code so it keeps making the most sense under the new circumstances
-
-
2
Commenters propose alternative framings of the rule
Lobsters commenters offer concrete experiences and reframings: andrewrk describes a stack-trace use case that depends on the pattern, azimux argues the deeper issue is damaging a function's public interface rather than the call itself, and telemachus questions whether the example should use two distinct private helpers instead of one.
“I feel like the better more general rule would be something like "do not damage the public interface unless absolutely necessary."”
— azimux -
The problem I see there is not that foo calls bar specifically, but rather that bar's public interface has been damaged as an implementation detail of foo as opposed to a tiny refactor. So I feel like the better more general rule would be something like "do not damage the public interface unless absolutely necessary." Though, if using the avoid…
2 more of the top 3 · 8 posts in this stretch
-
A concrete thing related to this I just ran into yesterday, is that when reporting certain kinds of errors, I collect a stack trace for later rendering to indicate which public function call was responsible for the creation of a particular artifact. You can make the first stack frame be the callsite of the public function by capturing the return…
-
I've been in the shoes of a developer adding optional function after optional function to make things "easier" for users. Some of the worst APIs grow this way. "Everything is in one function call" except that you've effectively reinvented having multiple functions by way of parameters. Also, "sanctified ink" is definitely a phrase I will be using…
-
-
1
Lobsters submission becomes the main discussion hub
The post is submitted to Lobsters by telemachus and gains 66 points and 13 comments, far outpacing the earlier and parallel Hacker News submissions, and becomes the primary venue for debate.
“By proactively making the shared implementation private, the tension is defused.”
— matklad, blog post author · source -
background
matklad publishes 'Avoid Public Functions Calling Other Public Functions' — The blog post proposes a rule of thumb: when a public function foo needs logic already in public function bar, extract a private helper for both to call, rather than having foo call bar directly, to keep the public API free of parameters added only for internal reuse.
What people are saying 16 voices from 1 site · best of 24 · verbatim
- Where exactly should the I/O boundary be drawn when deciding what to mock in tests?
- Would splitting the shared logic into multiple distinct private helpers (rather than one parameterized helper) better preserve the spirit of the rule?
- Nov 3
-
A place where I learned about this style and that was useful is https://sans-io.readthedocs.io/. It's targeted at networking protocols but you can usually extend it to any IO imo.
-
I read it the same as you and what I usually do is exercise all exceptions/faults indeed to make sure they're handled properly. It's sometimes difficult to put the system in a case to cause those issues while patching/mocking is a trivial one-liner (at least in Python which I'm used to).
- Nov 2
-
Recursive mutexes exist for pretty much exactly this reason. They are the easiest way to make a non-thread-safe library thread safe: every public API call grabs the mutex and releases it on return. They’re not quite as efficient as non-recursive ones, but they’re not that expensive: typically the recursion check is done on the failure case: try to…
-
That actually addresses the problem. The way that you capture a source location in C++ is to have a source location as a default-initialised parameter. If you have two public functions that do this, the one that calls the other just passes the source location rather than relying on the implicit value, so the reported source location will be the…
- Nov 1
-
IME and IMO if you have to worry about this at all it is a sign something else is wrong with your design. Maybe you shouldn't have public member functions for this at all. Maybe you should be using a more functional design. Maybe you should be using a less OO and more procedural design. Something is wrong.
-
This is especially true when some internal state is guarded by a mutex. In that situation there’s the danger of a function holding the mutex calling another function that acquires the mutex, causing deadlock. If you have a rule that public functions grab the mutex and internal functions can assume the mutex is held, _and_ the class never calls its…
-
But e.g. you wouldn't want to implement push by calling reserve, because the implementation of push already needs the branch to check whether it needs to grow, which reserve would duplicate.
-
Similar, in C++, the public function captures the callers source location and passes it around the implementation.
-
Thanks for clarifying! A couple more questions: > the trick here is architecting your application in a way that creates thin waist. Eg, you can have MyCustomeResourceStore, our you can can sprinkle raw SQL calls all other the place Should I interpret this as “both of these are examples of a thin waist” or is one meant to be thicker than the other?…
-
My tuppence worth: if foo() can be implemented in terms of bar(), then that suggests the API isn’t well factored. Maybe one of them is at a different level of abstraction? I note that features like Java’s default methods on interfaces encourage public methods to depend on other public methods, because that is all that is visible.
-
It depends: for example, in Rust's `Vec` there are a lot of pub methods that call other pub methods for a bunch of different reasons (safety, reuse, allocator API, ..).
- Oct 31
-
Data that comes from or goes outside the program itself. This can be a file, a socket, a TTY, environment variables, command line parameters, that type of stuff. When I made my [DNS library](https://github.com/spc476/SPCDNS) I explicitly separated packet receiving/sending from parsing/generating (you get a DNS packet, *then* send it to be decoded…
-
> Which is, imo, a natural, but horrible consequence of misguided approach to testing. You shouldn’t be mocking your methods, you should mock your IO Is mocking a `MyCustomResourceStore` “mocking your IO”, or does “mock your IO” refer to some lower layer of the I/O stack? Where does I/O begin?
-
Re: the private methods, I suppose I feel like either could be refactored into the other without much issue. I'm viewing it more as that one problem with extra stuff exposed through bar is that somebody outside could couple to that and now removing it breaks stuff. That is, we could improve the internal code-quality of the implementation as…
-
That was my thought too, but I assumed that it was just an example and that it shouldn’t be looked into too deeply.
-
I see how having both `foo` and `bar` call a private helper improves the public API. But I immediately wondered why not have *different* private helpers, `frobnicate` and `transmogrify`. (To put this another way: `helper(.frobnicate)` and `helper(.transmogrify)` struck me as swapping the original flag boolean for a flag enum.) I guess it all…