Adapter
Core. Expect to meet this one, and expect to be asked for it by name.
Start with the problem
A logging setup needs a new destination: an existing audit service that the security team already runs, with a client library already in the codebase.
public interface Appender {
void write(String line);
}
public final class AuditClient {
public void record(String who, String what, long whenEpochMillis) { /* sends to the service */ }
}
The obvious move is to hand logger.addAppender(auditClient, ...) and be done with it. That does not compile: AuditClient has no write(String) method, because it was written for a different caller with a different shape of call, before this logger ever existed.
Watch where it goes
One fix is to change AuditClient itself, adding a write(String) method that parses a line back apart into who, what, and when. That means editing code the security team owns and already has callers for, to serve a shape it was never designed around. A parsed-apart line is also a worse signal than the three fields AuditClient.record actually wants. Formatting a line and then picking it back apart throws away information the logger already had before it built the string.
The other option some people reach for is changing Appender itself, widening it to a shape AuditClient already fits. That fixes this one destination and breaks every other one. The console and file destinations were both written against write(String), and neither one knows anything about a who, a what, or a timestamp of its own.
The move
Write a small class that implements the interface the logger expects, and holds the thing that does not, translating one call into the other.
public final class AuditAppender implements Appender {
private final AuditClient client;
private final Clock clock;
public AuditAppender(AuditClient client, Clock clock) {
this.client = client;
this.clock = clock;
}
@Override
public void write(String line) {
client.record("logger", line, clock.now().toEpochMilli());
}
}
AuditAppender is the only class that knows both shapes exist. Logger calls write(String) the same way it calls every other appender, never learning that the destination underneath expects three arguments. AuditClient is untouched, so the security team's existing callers keep working exactly as they did before this logger ever knew it existed.
What modern Java changes here
An adapter with no state of its own beyond the thing it wraps is often small enough to write as a lambda, once the target interface has one method. AuditAppender needs a second collaborator, the clock, so a named class reads better here than a lambda would. Where an adaptee needs no extra argument at all, a method reference can be the whole adapter: legacyLogger::log satisfies a functional Appender-shaped interface directly, with no class and no lambda body to write.
The pattern shows up constantly at the boundary between a library and application code, wherever a third-party type does not implement the interface application code is written against. Executors.callable(Runnable) in the standard library is exactly this shape: a small adapter that lets a Runnable, which returns nothing, stand in wherever a Callable<Object> is expected. The one thing worth watching for is which side owns the interface. Adapter always keeps the target interface fixed and writes new code around the adaptee, never the other way around.
When naming it is wrong
If the only destination a logger will ever have already speaks write(String), there is no second shape to adapt and nothing to name Adapter yet. Writing a wrapper around a type that already implements the interface it needs to implement adds a class that forwards every call unchanged, for no reason a reader can find.
The threshold: reach for an adapter once a real, already-written type exists with the behavior you need but the wrong method shape. Changing that type has to be off the table too, because something else already depends on its current shape. A brand-new type, written for this one caller, should implement the interface directly instead. Wrapping it in an adapter it never needed is the Standard's D3 failure over-engineered (premature interface), aimed at a problem that did not exist yet.
Where this lives in the app
No corpus problem in this app currently wires in a third-party type this way; the closest treated material is syllabus item C5, which works through corpus/rate-limiter's interface-width decisions on LoggerApi and KeyBudget. Flagging this for whoever owns _index.json: anchoring Adapter to C5 does not hold up under inspection, since that lesson is about interface segregation, not about reconciling two mismatched interfaces. The example above is invented, kept in the logging domain so it sits next to observer.md and facade.md rather than introducing a new one.