ExerciseProduction incident
Production incident
The audit trail that stopped being written
45 minintermediate3–12 yrs
A real incident: symptom first, cause hidden, tradeoff at the end.
The incident
Compliance required every audited action to carry a correlation id, so the
platform team added an Auditable interface with a default method that
builds the label correctly. Every document type was made to implement it —
one line each, no other changes — and the tests passed.
Four months later an auditor pulls a sample and finds that invoices and
credit notes have no correlation id. Receipts and refunds do.
The code that writes the label is identical for all four. It is the same
default method. Nothing throws, nothing warns, and the one-line diffs all
look correct in review.
The difference is what the four classes extend.
Find the rule, fix every document type without deleting the base class's
method — other code still calls it — and then answer the design question:
should javac have warned here?
What this teaches
- The class always wins: a superclass method outranks an interface default
- The rule is the compatibility guarantee that made default methods possible
- Interface.super.method() is the only way to reach an outranked default
- Adding an interface to an existing class is not a one-line change if it carries defaults
- An override dispatches on the object, so casting to the supertype does not reach the base version
Starter
Starter.javaOpen in playground
import java.util.*;
/**
* Incident reproduction: the audit trail that stopped being written.
*
* Compliance required every audited action to carry a correlation id, so the
* platform team added an Auditable interface with a default method that
* builds the label correctly. Every document type was made to implement it.
* One line each, no other changes, and the tests passed.
*
* Four months later an auditor pulls a sample and finds that invoices and
* credit notes have no correlation id. Receipts and refunds do. The code
* that writes the label is identical for all four — it is the same default
* method — and nothing anywhere throws or warns.
*
* The difference is what the four classes extend.
*
* TASKS
* 1. Run it. Which document types lost their correlation id?
* 2. Work out why the SAME default method produced two different results.
* The rule has a name.
* 3. Fix it so every document type gets the compliant label, without
* deleting BaseDocument.auditLabel() — other code still calls it.
* 4. In a comment: javac gave no warning here. Should it have? Say what
* the compatibility rule buys, and what it costs.
*/
public class Starter {
/** The compliance requirement, as a default method. */
interface Auditable {
String documentId();
String correlationId();
default String auditLabel() {
return "[" + correlationId() + "] " + documentId();
}
}
/**
* The pre-existing base class. It has had auditLabel() since long before
* anyone had heard of correlation ids.
*/
abstract static class BaseDocument {
protected final String id;
protected BaseDocument(String id) {
this.id = id;
}
public String auditLabel() {
return id;
}
}
/* ── two types that extend the base class, and one line was added ── */
static final class Invoice extends BaseDocument implements Auditable {
Invoice(String id) {
super(id);
}
@Override public String documentId() { return id; }
@Override public String correlationId() { return "CID-INV-9"; }
}
static final class CreditNote extends BaseDocument implements Auditable {
CreditNote(String id) {
super(id);
}
@Override public String documentId() { return id; }
@Override public String correlationId() { return "CID-CN-4"; }
}
/* ── two types that extend nothing ── */
record Receipt(String documentId, String correlationId) implements Auditable {}
record Refund(String documentId, String correlationId) implements Auditable {}
public static void main(String[] args) {
List<Auditable> documents = List.of(
new Invoice("INV-1001"),
new CreditNote("CN-77"),
new Receipt("RCP-5", "CID-RCP-1"),
new Refund("RF-2", "CID-RF-3"));
System.out.println("── audit log ──");
List<String> labels = new ArrayList<>();
for (Auditable document : documents) {
String label = document.auditLabel();
labels.add(label);
System.out.printf(" %-12s %s%n", document.getClass().getSimpleName(), label);
}
long compliant = labels.stream().filter(l -> l.startsWith("[CID-")).count();
System.out.println();
System.out.println("documents audited : " + labels.size());
System.out.println("labels carrying a correlation: " + compliant);
System.out.println();
System.out.println("every document is compliant : " + (compliant == labels.size()));
System.out.println(compliant == labels.size() ? "PASS" : "FAIL");
}
}Run it locally:
cd exercises/java/oop/abstract-class-vs-interface/03-production
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Two of the four classes extend something. Which two, and what do they inherit that the others do not?
Hint 2
Both implementations exist in Invoice. Which one is being chosen, and by what rule — is this overriding, hiding, or something else?
Hint 3
You cannot call a default method with super.auditLabel(). There is a different syntax for reaching a specific interface.
Hint 4
Once it works, try casting an Invoice to BaseDocument and calling auditLabel(). Predict the answer first.
Done when
- All four document types emit a label starting with a correlation id
- BaseDocument.auditLabel() still exists and still serves non-Auditable types
- The fix uses Auditable.super rather than duplicating the default's body
- A comment states the rule by name and why it exists
- A comment answers whether javac should have warned, with a reason either way
Solution
Show the solution — try it yourself first
Solution.javaOpen in playground
import java.util.*;
/**
* Solution: the audit trail that stopped being written.
*
* The rule is "the class always wins". When a class inherits a concrete
* method from a superclass AND a default method of the same signature from
* an interface, the superclass method is used — silently, with no warning.
*
* That is not an oversight. It is the compatibility guarantee that made
* default methods possible at all: adding a default to an interface can
* never change how an existing class behaves. Without it, the JDK could not
* have added Collection.stream() or Iterable.forEach to interfaces that
* thousands of classes already implemented.
*
* So Invoice and CreditNote kept BaseDocument.auditLabel(), which predates
* correlation ids. Receipt and Refund extend nothing, so nothing outranked
* the default.
*
* THE FIX
* Override auditLabel() in the types that need it and delegate explicitly
* with Auditable.super.auditLabel(). That is the only way to reach a
* default method that something else is outranking, and writing it down is
* the point — the decision becomes visible instead of accidental.
*
* BaseDocument.auditLabel() stays, because other code still calls it.
*
* THE DESIGN QUESTION
* Should javac have warned? A warning on every class that inherits both
* would fire constantly and correctly in code where the class method is
* exactly what was wanted, so it would be noise. The real defence is that
* adding an interface to an existing class is not the one-line change it
* looks like: if the interface carries defaults, check each one against
* what the class already inherits.
*/
public class Solution {
/** The compliance requirement, as a default method. */
interface Auditable {
String documentId();
String correlationId();
default String auditLabel() {
return "[" + correlationId() + "] " + documentId();
}
}
/**
* The pre-existing base class. It has had auditLabel() since long before
* anyone had heard of correlation ids.
*/
abstract static class BaseDocument {
protected final String id;
protected BaseDocument(String id) {
this.id = id;
}
public String auditLabel() {
return id;
}
}
/* ── two types that extend the base class, and one line was added ── */
static final class Invoice extends BaseDocument implements Auditable {
Invoice(String id) {
super(id);
}
@Override public String documentId() { return id; }
@Override public String correlationId() { return "CID-INV-9"; }
/** FIX: reach past the inherited class method to the interface default. */
@Override public String auditLabel() {
return Auditable.super.auditLabel();
}
}
static final class CreditNote extends BaseDocument implements Auditable {
CreditNote(String id) {
super(id);
}
@Override public String documentId() { return id; }
@Override public String correlationId() { return "CID-CN-4"; }
/** FIX: same, and it has to be written on every affected subclass. */
@Override public String auditLabel() {
return Auditable.super.auditLabel();
}
}
/* ── two types that extend nothing ── */
/** Not Auditable, and not required to be. It still needs a label. */
static final class Memo extends BaseDocument {
Memo(String id) {
super(id);
}
}
record Receipt(String documentId, String correlationId) implements Auditable {}
record Refund(String documentId, String correlationId) implements Auditable {}
public static void main(String[] args) {
List<Auditable> documents = List.of(
new Invoice("INV-1001"),
new CreditNote("CN-77"),
new Receipt("RCP-5", "CID-RCP-1"),
new Refund("RF-2", "CID-RF-3"));
System.out.println("── audit log ──");
List<String> labels = new ArrayList<>();
for (Auditable document : documents) {
String label = document.auditLabel();
labels.add(label);
System.out.printf(" %-12s %s%n", document.getClass().getSimpleName(), label);
}
long compliant = labels.stream().filter(l -> l.startsWith("[CID-")).count();
// BaseDocument.auditLabel() was not deleted, and document types that
// are not Auditable still get the plain id from it.
//
// Note what you CANNOT do: casting an Invoice to BaseDocument does not
// get you the base version. auditLabel() is an instance method, so it
// is overridden, and overriding dispatches on the object rather than
// on the reference. Auditable.super is the only way back to a
// superclass or interface implementation, and only from inside the
// subclass itself.
BaseDocument plain = new Memo("MEMO-3");
System.out.println();
System.out.println(" a non-Auditable document still uses the base : "
+ plain.auditLabel());
System.out.println();
System.out.println("documents audited : " + labels.size());
System.out.println("labels carrying a correlation: " + compliant);
System.out.println();
System.out.println("every document is compliant : " + (compliant == labels.size()));
System.out.println(compliant == labels.size() ? "PASS" : "FAIL");
}
}Stretch
The fix has to be repeated on every affected subclass, which is the same
copy-paste the default method was meant to remove. Restructure so it cannot
be forgotten — either BaseDocument implements Auditable itself, or
auditLabel() is made final in one place and the varying part becomes a
separate method. Pick one, implement it, and say what you gave up.
← Back to When do you choose an abstract class over an interface?