Posted on July 9, 2026
This is the first installment of the series I announced in Writing code was never the expensive part: how to write code that wins the AI era — by recycling patterns that have been with us since the seventies.
The feature, in three sentences. A user wants to reset a forgotten password — nothing more. Every reset must be audited, and in some cases we block withdrawals right after it, for a simple security reason: an attacker who just took over the account must not be able to drain it. And the new part: whether to block is no longer decided by hardcoded rules in our code — we send the signals (IP, device fingerprint, balance at risk, 2FA) to a risk-scoring system and act on its verdict.
A password reset. You will not find a more boring piece of an application, and the ticket sounded like a few days of work. By the time we were done, the repo had gained a document with fifteen lessons about software design. And I claim that was no accident.
You do not learn design patterns from a catalog. Books give you the vocabulary — and you need the vocabulary, without it you cannot even talk about design. But the grammar, the instinct for when and why, you only get on live code, at the moment a pattern solves a pain that is yours. A pattern that never solved a concrete problem for you is interview trivia.
And that is exactly why a boring ticket is the ideal gym. On a password reset the domain does not distract you. No pricing engine, no settlements. All the mental capacity is left for form. (If this had been the core of a matching engine, we would have argued about latencies and form would have never come up, you know how it goes.)
The original code looked the user up by the reset code twice. The controller validated it once, and one layer down a service method found and re-validated the user from the very same string. Two DB calls, two validations, and between them room for each to behave differently.
// controller: validation #1 checkResetCodeAndReturnEmail(resetCode); // service, one layer down: lookup AND validation #2, from the same string User user = userDao.getByResetCode(resetCode);
Why? Because a String resetCode carries no trace of anyone having verified it. So we turned the successful validation into a type: UserWithResetCode wraps the already-loaded user, has a private constructor, and the only place it can be born is the validating factory. The instance is a proof that validation ran. Downstream methods do not want a string — they want the proof. Haskell folks call it Ghost of Departed Proofs, Alexis King calls it parse, don't validate. I call it common sense with a type system.
Repeated validation is not defensive programming. It is an admission that you are not using your type system.
New functionality tempts you into one particular move: add a parameter to the method. Audit log? boolean audit. Block withdrawals? boolean blockWithdrawals. It looks like this:
// every flag doubles the paths through the method
// and every caller must know its internals
void resetPassword(String resetCode, String password,
boolean audit, boolean blockWithdrawals) {
user.setPassword(password); // the reset itself
// ...
if (blockWithdrawals) {
user.setBlockedWithdraw(true); // + blocking
createSupportMessage(user);
}
if (audit) {
logAction(user, PASSWORD_RECOVERY); // + audit trail
}
}
Uncle Bob calls it a flag argument and pulls no punches in Clean Code: a boolean in a signature loudly announces that the function does more than one thing. And it never stops at two — the next concern arrives as the third flag, and the method has to be reopened, re-understood and re-tested every single time.
We wrapped each concern in a decorator instead. One concern, one wrapping class implementing the same interface; the reset itself shrank back to a pure state change:
public class AuditLoggingPasswordResetService implements UserPasswordResetService {
private final UserPasswordResetService delegate;
private final LogAuditAction logAuditAction;
@Override
public void resetPassword(UserWithResetCode userWithResetCode, String encodedPassword) {
delegate.resetPassword(userWithResetCode, encodedPassword); // the decorated behaviour
logAuditAction.logAction( // the added behaviour
new UserLogItem(userWithResetCode.user(), PASSWORD_RECOVERY, null, "Password reset"));
}
}
Adding the next concern is a new class plus one line in the composition — no existing class changes. And the decision whether to block? It does not enter the decorator as a flag. It selects the chain:
var auditedReset = new AuditLoggingPasswordResetService(passwordResetItself, logAuditAction);
var blockingReset = new WithdrawalBlockingPasswordResetService(auditedReset, withdrawalBlocker, messageBuilder);
(blockWithdrawals ? blockingReset : auditedReset)
.resetPassword(userWithResetCode, encodedPassword);
The blocking decorator blocks unconditionally. No if inside. Its presence in the chain IS the decision.
Fraud signals (IP, device fingerprint) are per-request data. And per-request data has an ugly tendency to leak where it does not belong: into shared interfaces as one more parameter, or into a ThreadLocal when things get desperate. With singleton-only wiring there is nowhere else for it to live — so EVERY implementation signs up for it:
// the request data leaks into the contract...
public interface UserPasswordResetService {
void resetPassword(UserWithResetCode userWithResetCode, String encodedPassword,
FraudSignals fraudSignals);
}
// ...and every implementor carries a parameter it does not use
public class AuditLoggingPasswordResetService implements UserPasswordResetService {
@Override
public void resetPassword(UserWithResetCode userWithResetCode, String encodedPassword,
FraudSignals fraudSignals) { // audit needs no fraud signals — dead weight forever
...
}
}
We refused that. Spring holds the singletons and one boundary bean — the composition root. That one composes the chain for each request with a plain new and closes over the signals. The interface stays clean, and the signals enter exactly one place:
// the router (a Spring bean) — the ONLY place that ever sees the signals
private UserPasswordResetService newProcessChain(FraudSignals fraudSignals) {
var blockDecision = new BlockDecision(fraudService, configurationService);
// ...
return (userWithResetCode, encodedPassword) -> transactionWrapper
.after(() -> blockDecision.shouldBlock(
userWithResetCode.user(),
signalsCollector.enrich(fraudSignals, userWithResetCode.user()))) // captured, not passed
.run(block -> ...);
}
The decorators, the reset and the interface never learn the signals exist. That is only possible because the chain is new-ed per request — a Spring singleton could hold them only as a widened parameter or a ThreadLocal.
One trap pays for this luxury: Spring annotations on a hand-built object silently do nothing. @Transactional on a new-ed decorator is the most dangerous kind of bug — the code reads as transactional and is not. Rule: objects outside the container carry no Spring annotations. None.
Our User entity is a classic: decades old, dozens of fields, everything everywhere all at once. The withdrawal blocker originally took the whole thing:
void block(User user, String supportMessage) { ... }
// what does it need from User? The signature claims: everything. The truth: the id.
A signature that takes the whole entity lies about its contract. You cannot tell what the method reads without opening it, and the test has to conjure a fully populated entity just to exercise one field. So we put a small facade in front of the entity — I call it a data view:
/** The exact data contract of a withdrawal block — the id, nothing else. */
static class UserForWithdrawalBlock {
public final long id;
UserForWithdrawalBlock(long id) { this.id = id; }
UserForWithdrawalBlock(User user) { this(user.getId()); } // mapping ctor: caller pays nothing
}
void block(UserForWithdrawalBlock user, String supportMessage) {
userDao.blockWithdrawals(user.id);
// ...
}
The signature stopped lying. The method declares exactly the data it needs, the mapping constructor keeps the call site clean (new UserForWithdrawalBlock(user)), and the test constructs a long instead of a forty-field entity. A public final field, no getters — it is a value, not an object with behaviour, and pretending otherwise is ceremony.
The same lie happens one level up, with services. The blocker needs to create one support message. The service that does it has a dozen methods; our audit service has about fifteen. Inject the whole thing and your class officially depends on fifteen methods to use one — every reader must discover which one, and every test must stub a colossus.
The fix is the Interface Segregation Principle with a trick that costs nothing:
// the role: exactly the behaviour the consumer needs
@FunctionalInterface
public interface CreateSystemMessage {
SystemSupportMessage createSystemMessage(long userId, String messageText);
}
// the god interface EXTENDS the role — every existing injection keeps working
public interface SupportMessageService extends CreateSystemMessage { /* the other dozen */ }
// the implementation does not change at all
public class SupportMessageServiceImpl implements SupportMessageService { ... }
The consumer now declares CreateSystemMessage, Spring still injects the same bean, and nobody else even noticed. We made the same move for the audit trail: LogAuditAction, one method carved out of a fifteen-method service. And because both roles are functional interfaces, you already know what the stubs look like in the test section below: one lambda each. That is not a coincidence. It is the same property — a narrow, honest contract — seen from two sides.
A call to the fraud system takes hundreds of milliseconds, sometimes more. The reset and the block must be atomic, so they belong in a transaction. What absolutely must not happen: a network call holding an open DB transaction, stretching it with its latency. So we made the transaction an explicit seam with a builder contract:
transactionWrapper
.after(() -> blockDecision.shouldBlock(user, signals)) // decided BEFORE the transaction
.run(block -> ...); // executed inside it
The decision is resolved outside; the transaction merely executes a finished plan. Functional core, imperative shell — just in Java, wrapped around a relational database.
And while we are on latencies: a read timeout is not a deadline. One second sounds safe, except SO_TIMEOUT measures the silence between bytes. If the other side sends you one byte every 900 milliseconds, the connection will happily run for a week and the timeout never fires. We enforced a real deadline with future.get(deadline) over a bounded thread pool — which doubles as a bulkhead: when the fraud system gets slow, it saturates eight threads, not the whole server. If you have not read Release It!, this chapter will hand you the bill one day.
The whole new process sits behind a feature flag, evaluated per user. One single if on the flag in the entire feature, in a router at the boundary — neither path knows it is flagged:
// PasswordResetRouter — the only place in the codebase that reads the toggle
public void resetPassword(UserWithResetCode userWithResetCode, String encodedPassword,
FraudSignals fraudSignals) {
if (isNewProcessEnabled(userWithResetCode)) {
newProcessChain(fraudSignals).resetPassword(userWithResetCode, encodedPassword);
} else {
// legacy path: byte-for-byte the version running in production today
userService.resetPassword(userWithResetCode.resetCode(), encodedPassword);
}
}
The legacy branch is the original master code, verbatim. And here I will confess the thing that almost happened to us: we first restored the "original" method from our own branch history — from a commit where the blocking logic had already been extracted out of it. It looked old, it compiled, the tests were green. Flag OFF would have silently shipped a password reset without withdrawal blocking. Review caught it.
A kill switch that shares code with the thing it guards, guards nothing. Verify the fallback path by diffing against production, not against an intermediate state of your own branch.
The last safety belongs to the same family: when the fraud system is down, the fallback is maximum risk, not zero. Fail safe, not fail open. A scoring outage must not open the door — otherwise we might as well hang a sign on the login page saying "no checks today".
Here is where all of the above starts paying rent. When every concern is a small class behind a role interface, a test does not need a mocking framework — it needs a lambda. This is the complete test of the audit decorator:
@Test
void resetPassword_logsPasswordRecoveryAuditActionForTheUser() {
AtomicReference<LogItem> loggedAction = new AtomicReference<>();
var service = new AuditLoggingPasswordResetService(
(userWithResetCode, encodedPassword) -> { }, // the decorated reset: a no-op lambda
loggedAction::set); // the audit role: a recording lambda
User user = new User();
user.setId(1L);
service.resetPassword(UserWithResetCodeTestHelper.proofFor(user, "code"), "encoded");
// the WHOLE value, compared by equals against a hand-built expected instance
assertEquals(
new UserLogItem(user, PASSWORD_RECOVERY, null, "Password reset"),
loggedAction.get());
}
Three things to notice. First, no @ExtendWith(MockitoExtension.class), no when/verify choreography — the role interfaces from earlier are functional, so a stub is one line and a recording stub is a method reference. When a test needs a mock, the code is telling you a seam is missing.
Second, the proof is real. UserWithResetCodeTestHelper lives in the test source set in the proof's package and mints the instance through the real validating factory — no mocking, no reflection. The private-constructor guarantee holds even in tests; a test cannot fabricate a proof any more than production code can.
Third, the assert compares the whole UserLogItem by equals against a hand-built expected instance. The getter-picking alternative — assert the action, assert the user — covers only the fields the author thought of; a field added next year ships untested without a whisper. The whole-value assert covers everything and fails with one readable diff. The expected object IS the specification.
The same trick scales up. The test of "reset first, then block" is one recorded event stream — the delegate lambda appends "reset", the blocker records "blockWithdrawals 1", and the assert is a single line:
assertEquals(List.of("reset", "blockWithdrawals 1"), events);
Delegation, ordering and the target user, verified in one comparison of one value. That is what code prepared for change looks like from the test's side of the glass.
This is not a call to sprinkle patterns everywhere. Not one of the fifteen lessons was born from "let's use a decorator". Each arrived as an answer to a concrete pain, usually voiced in code review rather bluntly. A pattern is an answer — when no question has been asked yet, it is just noise and future archaeology. On a CRUD screen where nothing worse than a typo can happen, this construction would be a cannon aimed at a sparrow, and I would be the first to shoot it down in review.
It aims at something else: the habit of treating boring tickets as work to "get out of the way". That is exactly where the craft is passed on at the lowest price.
This is the entire feature, condensed to the methods that matter (class each line lives in noted in the comments):
// UsersForgottenPasswordController — the proof is minted ONCE, at the boundary
UserWithResetCode proof = userService.findByValidResetCode(dto.getResetCode())
.orElseThrow(ResetPasswordException::new);
passwordResetRouter.resetPassword(proof,
encode(proof.user().getEmail(), dto.getNewPassword()),
new FraudSignals(ipAddress, dto.getDeviceFingerprint()));
// PasswordResetRouter — the single feature-flag if
if (isNewProcessEnabled(proof)) {
newProcessChain(fraudSignals).resetPassword(proof, encoded);
} else {
userService.resetPassword(proof.resetCode(), encoded); // legacy, verbatim master
}
// PasswordResetRouter.newProcessChain — per-request composition root
return (proof, encoded) -> transactionWrapper
.after(() -> blockDecision.shouldBlock(proof.user(),
signalsCollector.enrich(fraudSignals, proof.user()))) // risk-system call BEFORE the tx
.run(block -> (block ? blockingReset : auditedReset) // presence = decision
.resetPassword(proof, encoded));
// WithdrawalBlockingPasswordResetService — decorator, blocks unconditionally
delegate.resetPassword(proof, encoded);
withdrawalBlocker.block(new UserForWithdrawalBlock(proof.user()),
blockMessageBuilder.messageFor(proof.user()));
// AuditLoggingPasswordResetService — decorator, audit trail
delegate.resetPassword(proof, encoded);
logAuditAction.logAction(new UserLogItem(proof.user(), PASSWORD_RECOVERY, null, "Password reset"));
// UserServiceImpl — the reset itself, a pure state change
user.setPassword(encoded);
user.setPasswordResetCode(null);
user.setPasswordResetTimestamp(null);
And the shape of a request, with the composition tree:
HTTP PUT /reset-password
│
▼
Controller ── userService.findByValidResetCode(code) ──► UserWithResetCode (the proof)
│
▼
PasswordResetRouter (Spring bean; the ONLY if on the feature flag)
│
├─ flag OFF ──► UserServiceImpl.resetPassword(code, pwd) legacy, verbatim master
│
└─ flag ON
│
▼
transactionWrapper.after( blockDecision.shouldBlock(…) ) risk-system call, before the tx
│ resolved boolean
▼
.run( block ? blockingReset : auditedReset ) transaction starts here
composition, built per request with plain `new`:
blockingReset = WithdrawalBlocking( blocks + support message
AuditLogging( audit trail
UserServiceImpl )) the reset itself
auditedReset = AuditLogging(
UserServiceImpl )
A few-day ticket left behind a document my colleagues actually read, and a merge request where every pattern points at the line of code that defends it. Your codebase has its own password reset — the most boring item in the backlog. If all you extract from it is the ticket, that is a wasted classroom.
Fifteen lessons will not fit into one article and I refuse to list them like a phone book. Growing a signal record instead of method signatures, fail-safe defaults, splitting a monster service into small intent-named collaborators — those I am saving for the next installment. Gotta have something to write about.
Sources: Matt Noonan, Ghosts of Departed Proofs; Alexis King, Parse, don't validate; Robert C. Martin, Clean Code (ch. 3, Flag Arguments); Martin Fowler, FlagArgument; Pete Hodgson, Feature Toggles; Mark Seemann, Composition Root; Michael Nygard, Release It!; GoF, Design Patterns (Decorator).