Facade
Core. Expect to meet this one, and expect to be asked for it by name.
Start with the problem
An API gateway needs to check whether a request may proceed under a rate limit. A single budget, checked and charged directly, is a small piece of code right at the call site.
KeyBudget budget = budgets.get(clientKey);
budget.lock();
try {
if (budget.hasRoom()) {
budget.charge();
return Decision.allow();
}
return Decision.deny(clientKey.value());
} finally {
budget.unlock();
}
For one budget and one caller, this reads fine, and the lock discipline is visible right where it matters.
Watch where it goes
A shared backstop arrives: every client also draws down a cap the whole service shares, on top of its own key's limit. A request now has to satisfy two budgets, not one. The gateway has to check both have room before charging either, or a request that fails the backstop can still spend a client's own budget on the way there.
Getting that right at the call site means several things at once. Both budgets have to lock in a fixed order, to avoid a deadlock against a second request locking them the other way round. Both have to be checked before either is charged. Both have to release their lock in reverse order, even if something throws in between. Every caller that wants a decision has to reproduce that whole protocol correctly. A second caller, a background job checking remaining capacity without charging anything, needs most of the same protocol again, minus the charge.
The move
Put the whole check-lock-charge protocol behind one class with a small set of methods that state what a caller wants, not how the budgets underneath are held.
final class RateLimiter {
private final Scopes scopes;
private final Map<ClientKey, KeyBudget> budgets = new HashMap<>();
Decision tryAcquire(ClientKey key) {
List<Scoped> scope = resolve(key);
return locked(scope, () -> decide(scope));
}
int remaining(ClientKey key) {
List<Scoped> scope = resolve(key);
return locked(scope, () -> fewestRemaining(scope));
}
}
This is close to the shape behind corpus/rate-limiter's RateLimiter. A caller asks for tryAcquire or remaining and never learns that either one might touch one budget or several, in what order, or under what lock discipline. decide checks every budget in scope before charging any of them, and locked acquires every budget in a fixed order and releases them in reverse, whatever the body does. Both live once, inside RateLimiter, instead of once per caller.
What modern Java changes here
Nothing about this facade needs an interface separate from the class, since RateLimiter here has exactly one implementation and no second one is in view. A facade earns its keep from what it hides, not from how many types back it. A single final class with a narrow set of public methods is the ordinary Java shape, not a compromise on the pattern. Java's own module system offers the same idea one level up: a module exports specific packages and keeps the rest unreachable from outside, however many classes that rest contains.
The private helper methods carry the actual complexity here. resolve, decide, and locked are all private. RateLimiter has three collaborating steps instead of one, and that structure stays invisible to every caller. KeyBudget's lock protocol used to be visible to all of them, before this class existed.
When naming it is wrong
A single budget, checked once, with no backstop and no second caller needing the same protocol, does not need a facade in front of it. The seven-line block at the top of this page, called directly from one place, is already the whole answer. Wrapping it in a class before a second concern or a second caller exists adds a layer nobody reads through.
The threshold: reach for a facade once two or more collaborators have to be driven together, in an order or under a discipline a caller should not have to know. The other trigger is a second caller already needing the same sequence. One collaborator, one caller, and a class wrapped around a single method call is over-engineered (premature interface) under the Standard's D3 dimension.
Where this lives in the app
Syllabus item C5 works through corpus/rate-limiter's KeyBudget and its wider-than-strict lock surface. RateLimiter is the facade that owns the multi-budget locking protocol, so no caller has to reproduce it. The lesson's own reasoning is that this surface stays deliberately wide for exactly one caller, RateLimiter, that needs it.