# NOTES — eleven files, one abstract class, and the field that justifies it

## Run it first

From the directory holding the sources:

```
..\..\..\.toolchain\jdk-21\bin\javac.exe -d out *.java
..\..\..\.toolchain\jdk-21\bin\java.exe -cp out Main
```

`javac -Xlint:all` on these eleven files prints nothing and exits 0. Real output, verbatim:

```
--- 1. four implementors of one interface, none of them related
    console | connected
  lambda and method ref captured : [connected, connected]
  the lambda's class is synthetic: true (no source file exists for it)
  the method ref is synthetic too: true
  the other two are             : ConsoleAppender and JsonDestination
--- 2. the shared field, maintained in exactly one place
    console | first
    console | second
  console.written()             : 2
  list.written()                : 3
  list.lines()                  : [a, b, c]
  one count per object, not one shared by the class
--- 3. one object registered as both halves of a destination
  json.lines().size()           : 1
  {"at":"2026-08-18T09:15:00Z","level":"INFO","thread":"main","msg":"connected"}
  is an Appender                : true
  is a Formatter                : true
--- 4. flush(): one override, three inherited no-ops
  before flushAll, buffered has : 1 pending, 0 delivered
  immediate already has         : [[WARN] retrying a flaky request]
  after flushAll, buffered has  : 0 pending, 1 delivered
  delivered                     : [[WARN] retrying a flaky request]
  the lambda survived flushAll  : [[WARN] retrying a flaky request]
--- 5. writeAll(): a body on an interface, running on a lambda
  a one-expression Appender took : 3 lines
  in order                       : [connected, retrying, gateway timed out]
  MAX_LINE_CHARS                 : 4096
  5000-char message arrived as   : 4096 chars
--- 6. what javac made of the members we declared
  declared  int MAX_LINE_CHARS = 4096;
  really is public static final int
  declared  void write(String formattedLine);
  really is public abstract
  declared  default void flush() { }
  really is public, isDefault=true
  Appender: 1 field(s) declared, 0 of them per-instance
  CountingAppender: 1 field(s) declared, 1 of them per-instance
  JsonDestination's superclass   : Object, interfaces it implements: 2
```

Four lines in there are the lesson. The two field tallies in block 6, both `true` lines in block 3,
and `after flushAll, buffered has : 0 pending, 1 delivered` in block 4.

Then look at `out/`:

```
Appender.class          CountingAppender.class  Level.class      Main.class
BufferedAppender.class  Formatter.class         ListAppender.class
ConsoleAppender.class   JsonDestination.class   LogRecord.class
MiniLogger$Registration.class  MiniLogger.class
```

Twelve class files from eleven sources, and the extra one is `MiniLogger$Registration`, the nested
record. No `$1`, `$2`, `$3`: the two lambdas in `Main` are not anonymous classes and produce no
files. They are spun up at run time from an `invokedynamic` call site, which is why block 1 reports
them as synthetic and why they have no name worth printing.

---

## `Formatter.java` — the case with nothing to decide

```java
public interface Formatter {
    String format(LogRecord record);
}
```

**Copied from the contract unchanged, and it is the whole file.** No state, no shared code, one
method, several implementations. Making this an abstract class would take away every lambda that
implements it. `Main` block 4 has one, `record -> "[" + record.level() + "] " + record.message()`,
and `Demo.java` in the corpus has two more.

The guarantee `interface` gives here that a C++ pure abstract base class does not: **no implementor
inherits any state from it, ever, because the language forbids the field.** In C++ the same promise
is a convention that the next person to edit the header can break by adding a member.

---

## `Appender.java` — three kinds of member, three different reasons

```java
    int MAX_LINE_CHARS = 4096;
```

**Implicitly `public static final`, and block 6 prints those three words back.** One value for the
whole program, readable as `Appender.MAX_LINE_CHARS` from anywhere.

What it guarantees: no destination can be handed a line longer than this, because `MiniLogger.offer`
truncates against it and every destination goes through `offer`. What breaks without the constant:
the bound has to be repeated at every call site, and the fifth one gets it wrong.

What it cannot be: per-instance. There is no way to give one destination a different limit here.
The moment you want that, the value belongs in a constructor parameter, and that is J4.

```java
    void write(String formattedLine);
```

**The only abstract method, and keeping it the only one is a design constraint rather than an
accident.** Block 6 reports it as `public abstract` without either word being written.

One abstract method is what makes `Appender` a functional interface, so `line ->
System.out.println(line)` and `captured::add` are both legal destinations. Add a second abstract
method and every lambda in the corpus stops compiling. `from-cpp.md` has the three errors.

```java
    default void flush() {
    }
```

**The empty body is the answer, not a placeholder.** A destination that holds nothing has nothing to
hand over, so doing nothing is correct for it.

What this guarantees: `MiniLogger.flushAll` can call `flush()` on every registration without knowing
what any of them is. What breaks if this is abstract instead: three compile errors across two files
that worked yesterday, including both lambdas. What breaks if it is not on the interface at all:
`flushAll` needs `instanceof BufferedAppender`, and it has to be edited for the second kind of
buffered destination.

```java
    default void writeAll(List<String> lines) {
        for (String line : lines) {
            write(line);
        }
    }
```

**It calls `write` and touches no field, which is the test for whether a body belongs on an
interface.** A `default` has no state available to it: the only fields in scope are the interface's
own, and those are static and final.

Note it calls `write` rather than doing the work itself. That is what keeps `CountingAppender`'s
counter correct through this path, and `GapTest` asserts it: `list.writeAll(List.of("a", "b"))`
leaves `written()` at 2. A version that reached past `write` would deliver the lines and lose the
count. That bug shows up as a metric being wrong, not as a log line going missing, so it gets
diagnosed a long way from here.

---

## `CountingAppender.java` — the abstract class, and its one reason to exist

```java
public abstract class CountingAppender implements Appender {

    private int written;
```

**This field is the entire argument for the class.** An interface cannot declare an instance field,
so a shared mutable count has nowhere else to live. Block 6 states it in two lines: `Appender` has
one field and none of it is per-instance, `CountingAppender` has one and it is.

`implements Appender` and not "instead of": the contract stays on the interface, so
`BufferedAppender` can satisfy it without coming near this class.

```java
    @Override
    public final void write(String formattedLine) {
        written++;
        doWrite(formattedLine);
    }
```

**`final` is the load-bearing word.** The invariant: after n calls to `write` on an instance,
`written()` is n, and all n lines reached `doWrite` in order.

Drop `final` and a subclass can override `write`. Then some lines are counted and some are not, and
nothing warns you. The symptom is a count that disagrees with the log, and the count is usually what
somebody is alerting on.

This is the template method shape, and it is the reason to prefer an abstract class over a
`default`: `final` on a `default` method is refused with `error: modifier final not allowed here`. An
interface cannot stop an implementor replacing the sequence.

```java
    protected abstract void doWrite(String formattedLine);
```

**`protected` and `abstract` together say: subclasses must answer this, nobody else may call it.**
`abstract` moves "did you decide where lines go?" from a review comment to a compile error:

```
M3.java:4: error: ListAppender is not abstract and does not override abstract method doWrite(String) in CountingAppender
final class ListAppender extends CountingAppender {
      ^
1 error
```

Give it a body instead and that error disappears along with the guarantee. A subclass that forgot
would then silently discard every line.

**Not thread-safe, and this matters against the real contract.**
`corpus/logger/contract/Appender.java` says `write` may be called for one instance from many threads
at once. `written++` is a read, an add and a write, so two threads can lose an increment between
them. Nothing in this lesson takes a lock, because the fix belongs to J12. Say that out loud if you
write this class in a round; an unqualified "I keep a count" invites the follow-up.

---

## `ConsoleAppender.java` and `ListAppender.java` — the second implementation

```java
public final class ConsoleAppender extends CountingAppender {
    @Override
    protected void doWrite(String formattedLine) {
        System.out.println("    console | " + formattedLine);
    }
}
```

**Two lines of its own, and that ratio is the argument for the base class.** One subclass would not
be. With a single implementation the counter belongs inside that class, and the abstract class is a
file to open for nothing.

```java
    /** A copy, so a caller cannot add lines the counter never saw. */
    public List<String> lines() {
        return List.copyOf(lines);
    }
```

**The copy keeps `written()` and `lines()` consistent.** Hand out the live `ArrayList` and a caller
can add a line the counter never saw, so `written()` and `lines().size()` disagree. That is A7's
subject and it costs one method call here.

`final` on both classes: no third level, so nothing can override `doWrite` again. One abstract class
with concrete leaves is a shape you can defend. A three-level hierarchy is one somebody will ask you
to justify.

---

## `BufferedAppender.java` — the implementor that proves the interface

```java
public final class BufferedAppender implements Appender {

    private final List<String> pending = new ArrayList<>();
    private final List<String> delivered = new ArrayList<>();
```

**`implements Appender`, not `extends CountingAppender`, and that is the point of the file.** It has
two fields of its own and no use for a counter. Under a mandatory base class it would inherit a
field it does not want and spend its one `extends` on it.

This is the shape curveball 03 asks for. `corpus/logger/curveballs/03-rolling-appender/` wants a
destination that starts a new file every n lines, and the reference absorbs it in **zero lines**,
measured in that curveball's `PATCH.md`. Zero, because a destination is one method and its own state
is nobody else's business.

```java
    @Override
    public void flush() {
        delivered.addAll(pending);
        pending.clear();
    }
```

**The `clear()` is what makes `flush` repeatable.** The invariant: after `flush()` returns,
`pending()` is 0, and every line written before the call is in `delivered()` in order.

Drop the `clear()` and a second `flush` delivers everything a second time. `GapTest` calls `flush`
twice for exactly that reason. Duplicate log lines are the kind of bug that gets diagnosed as a
retry loop somewhere else entirely.

**This is the one override of a `default` in the lesson, and it is one of five destinations.** That
ratio is the honest case for `flush()` being a `default` rather than abstract.

---

## `JsonDestination.java` — one object, both parameters

```java
public final class JsonDestination implements Appender, Formatter {
```

**One class declaration, two contracts, and it compiles because both are interfaces.** Block 3
prints `is an Appender : true` and `is a Formatter : true` for the same instance, and block 6
confirms its superclass is `Object`.

Turn either interface into an abstract class and this line fails on the comma, with
`error: '{' expected`. The class had no superclass to spare, and it did not need one.

```java
        logger.addAppender(json, Level.INFO, json);
```

**The same reference passed twice, to two parameters of two different types.** That is what an
interface buys and a base class cannot. A type can be several things at once, as long as none of
those things owns a field in it.

Worth knowing where the limit is. This is fine when the two roles are genuinely one decision, which
is the case here: this destination's format exists only for this destination. When the format is
reusable, keep it separate. `Demo.java` in the corpus does, and registers one formatter against two
appenders.

```java
    @Override
    public String format(LogRecord record) {
```

**No field read and no field written, so `format` is a pure function of the record.** That is what
lets `MiniLogger` call it once per destination in any order, and what makes the same record safe to
hand to every destination without a copy.

---

## `MiniLogger.java` — the class that knows two type names

```java
    private record Registration(Appender appender, Level threshold, Formatter formatter) {
    }
```

**Two interfaces and an enum, and no concrete destination class named anywhere.** This is
`corpus/logger/reference/src/Registration.java` cut down, and the reason it can be a nested `private`
record is that nothing outside `MiniLogger` needs it.

What that guarantees: adding a destination kind needs no edit here. What breaks if `Registration`
holds `CountingAppender` instead of `Appender`: `BufferedAppender` and every lambda stop being
registrable, and the file has to be edited for each new kind.

```java
        if (line.length() > Appender.MAX_LINE_CHARS) {
            line = line.substring(0, Appender.MAX_LINE_CHARS);
        }
```

**The constant is read from the interface, so the bound and the contract cannot drift apart.** Block
5 logs a 5000-character message and the destination receives 4096.

```java
    public void flushAll() {
        for (Registration registration : registrations) {
            registration.appender().flush();
        }
    }
```

**This loop is the reason `flush()` had to be on `Appender`.** The only static type available here is
`Appender`, and this method cannot ask a registration what it really is.

What breaks without the `default`: this loop needs `instanceof BufferedAppender`, and then a cast,
and then an edit for every future kind of destination that holds lines. Block 4 shows all three
destinations going through the same loop, with one of them doing work and two of them doing nothing.

---

## The threshold, stated so it is decidable

Under interview time pressure, the question is only ever which of two things you need. Ten seconds:

**Name what the implementations share. A field means an abstract class. A signature means an
interface. Nothing shared means an interface.**

The longer form, in the order the questions matter:

1. **Is there anything to share at all?** One method, no state: interface, and stop. This is
   `Appender`, `Formatter`, `Clock` and `LoggerApi`, which is four of the four contracts in this
   corpus problem.
2. **Is the shared thing a field?** Yes: abstract class, and only the field goes in it. This is
   `CountingAppender`, and its one field is the whole justification.
3. **Is the shared thing code with no state?** A `static` helper method, or a `default` if every
   implementor should have it. `writeAll` is the second case.
4. **Must the sequence be unbreakable?** An abstract class with a `final` method calling `abstract`
   hooks. `final default` does not exist, so an interface cannot make this promise.
5. **Do you know every implementor and want the compiler to keep checking?** A `sealed interface`,
   which is J11.

And the cost of getting it wrong in each direction. An unnecessary abstract class spends the one
`extends` its subclasses have, and you find out when one of them needs a second role. An interface
with one implementation and no second in sight is what the `over-engineered` tag is for.

---

## What an interviewer is measuring

Not whether you know the keywords. Two things:

**Whether you can say why the state is where it is.** "The count is a field, and only a class can
hold a field, so the count lives in `CountingAppender` and the contract stays on `Appender`" is a
design sentence. "I used an abstract class for reuse" is not, and it invites a follow-up you have
not prepared.

**Whether your interfaces have more than one implementation.** `Appender` has four classes behind it
in this lesson plus two lambdas. In the corpus problem itself exactly one class implements it,
`RollingAppender` in `curveballs/03-rolling-appender/tests/`, and every other destination there is a
lambda or a method reference. An interface with one abstract method is what allows that, and it is
what makes this a seam rather than a layer.

The measured version: curveball 03 costs the reference **0** lines and curveball 02 costs it **10**,
from each one's `budget.json`, and neither of them reaches past `Logger.java`.
