GUIDE — Java under the Discipline, v0.1 (trunk)
01Status. Beta; sixth language. Java ships as a trunk plus three overlays, in the owner's priority order: this trunk, then GUIDE-JAVA-SPRING, GUIDE-JAVA-GRAALVM, GUIDE-JAVA-JAKARTA. An overlay presupposes the trunk and rewires only the sections it names; everything unstated is inherited. Composition matrix: SPRING+GRAALVM legal (Boot AOT); JAKARTA+GRAALVM legal (build-time CDI implementations); SPRING+JAKARTA illegal (one container per target). Sections remain isomorphic to the other language guides — diff them.
02Framing note — the sixth point of the typology. Rust enforces; TS permits but compiles; Python trusts; C++ demands a subset; Go prescribes; Java bifurcates: the strictest mainstream language runs on the most reflective mainstream runtime. Java is two languages — the one javac type-checks, and the one frameworks assemble at runtime out of reflection, dynamic proxies, classpath scanning, and bytecode weaving. The Discipline's whole job here is to keep cells inside the first language; the overlays govern how the second one is allowed to surround them. Patterns dissolve mid-way: Strategy = interface + registry; Visitor = sealed hierarchy + exhaustive switch (Java 21 finally has sum types); Decorator = wrapper; Observer = explicit listener seam; Singleton = forbidden — private static final INSTANCE is its Java disguise, and the framework-managed bean is its modern one.
03Scope honesty. Long-lived backend/system code. Build scripts, one-off tools, and test scaffolding follow lighter rules. Code generated by annotation processors follows the generated-code rule: the processor's input is the taggable unit; outputs are excluded, and processors that generate domain logic are infra-only (the proc-macro parallel).
0. Language baseline
- 04Version floor: Java 21 LTS; target 25 LTS.
--releasepinned;-Werror; preview features off by default (a carrier may opt in per feature, documented). - Compile-time gates: Error Prone (Apache-2.0) MUST; nullness via JSpecify annotations (Apache-2.0) + NullAway (MIT) —
@Nullableis part of the seam contract, unannotated null crossing a seam is a build failure. Formatting via Spotless with a single pinned formatter — language-adjacent, zero discipline budget. - Architecture tests are conform checks: ArchUnit (Apache-2.0) rules are committed as ordinary tests — sibling-import bans, ring rules ("cells import no framework"), naming grammar. This is the Java T-syn engine, running where developers already live.
- Build: Maven primary (Enforcer plugin: dependency convergence, banned-dependency list, version pinning; reproducible-build configuration on) — boring and declarative; Gradle MAY, with dependency locking + version catalogs. Either way the dependency graph is locked (A2).
- Suppression policy (xfail-strict posture): bare
@SuppressWarningsis banned; suppressions carry the narrowest scope, the specific check name, and a// reason. Stale suppressions are swept by a conform check that re-runs the named check against the suppressed element — same mechanism as the C++ NOLINT sweep. - Tests: JUnit 5 — license flag: EPL-2.0, test-only, acceptable under the Charter's case-by-case zone; AssertJ (Apache-2.0). Property testing: jqwik (EPL-2.0, same flag) or QuickTheories (Apache-2.0).
- Boundary validation (parse, don't validate): JSON/HTTP/DB inputs decode into boundary DTOs (records) with explicit validation, then convert into domain types. Jackson and friends live at the boundary; their annotations never appear on domain types.
1. Cells
05A cell is a Maven/Gradle module by default; in small repos a package guarded by ArchUnit rules is the starting granularity, with promotion to a module on the usual triggers (heavy optional deps, release cadence, ~2 kLoC). JPMS module-info SHOULD be used where the toolchain cooperates — it is the compiler-enforced form of the surface rule — with the ecosystem friction named honestly rather than wished away.
- 06Import-is-execution, Java edition — lazier and therefore worse: class initialization runs on first touch, in an order no one can read from the source.
static { }blocks with side effects, mutable static fields, eager singletons, and registration-in-initializer are banned in cells. The one tolerated static:private static final Logger(fighting it is wish-ratio poison) — but log sinks are configured at the boundary. ServiceLoaderis native registration magic (META-INF/services— Java's inventory/linkme): banned for cells, legal only at boundary plugin points.- Platform capabilities are injected, and the JDK itself blesses the pattern:
java.time.Clock(the stdlib's own injectable clock),java.nio.file.FileSystem,java.net.http.HttpClient,RandomGenerator— constructor parameters, never ambient.System.getenv/System.exit/Runtimeare banned in cells. - Values crossing seams are records (immutable by construction); collections crossing seams are unmodifiable.
07Cell manifest (annotation carrier, §5):
08@SpecImplements(value = "spec://org.vibevm.core/vibevm/modules/vibe-resolver/PROP-003#solver-upgrade", r = 2)
@Cell(seam = "DepSolver", variant = "sat", replaces = "naive", flag = "solver")
public final class SatDepSolver implements DepSolver {
private final DepProvider provider;
private final Clock clock;
public SatDepSolver(DepProvider provider, Clock clock) { ... }
}
2. Seams
- 09A seam is an
interfacein the seams module. Conformance is nominal (implements) — Java needs no Go-style assertion; the declaration is the declaration. - Composition over inheritance is a MUST at seams: no abstract base classes with behavior, no template methods (R-021).
defaultmethods are legal only for trivial combinators/adapters, never business flow. - Closed sets are sealed interfaces/hierarchies; open extension points are ordinary interfaces. The distinction is part of the seam's text.
- Seam methods that can fail in expected ways return the seam's sealed result type (§4);
@Nullableappears explicitly wherever null is part of the contract;Optionalis a return-type-only construct (never fields or parameters).
3. Registry and flags
10R-001 binding — flag at the seam, never in the veins:
11// registry module — the only flag reader and the only module that
// depends on cell modules.
public static DepSolver depSolver(AppConfig cfg, DepProvider p, Clock clock) {
return switch (cfg.solver()) { // provenance: default | env | cli | lockfile
case SAT -> new SatDepSolver(p, clock);
case NAIVE -> new NaiveDepSolver(p);
};
}
- 12Two tiers, never confused: the assembly tier — which modules are on the classpath/module-path (Maven profiles, optional dependencies) — answers "is the code in the artifact"; the runtime tier is a config record, constructor-bound, read once in
main. An assembly choice must not change a seam's surface; a runtime flag must not require reassembly. (The GraalVM overlay shows these tiers migrating — see its §3.) - No
ServiceLoader, no reflection-based wiring, no classpath scanning in the trunk; the overlays legalize container machinery as this composition root and nowhere else.
4. Errors as contract
13Java's historical irony, handled with respect: the language invented the signature-level failure surface — checked exceptions — and the culture buried it under wrappers and lambda-hostility until throws clauses became lies. The Discipline does not resurrect them; it takes the modern equivalent the language finally grew:
- 14Expected failures are sealed result types at seams, consumed by compiler-checked exhaustive
switch:
15public sealed interface SolveResult permits SolveResult.Ok, SolveResult.Err {
record Ok(Resolution resolution) implements SolveResult {}
record Err(SolveError error) implements SolveResult {}
}
public record SolveError(Code code, String spec, Throwable cause) {
public enum Code { CYCLE, UNSATISFIABLE }
}
16spec carries the violated REQ URI; boundary rendering appends it (PROP-014 §2.6). No default arm on a sealed switch — the default clause silences the compiler's exhaustiveness check, the same graveyard move banned in the C++ Traditional guide; a new permitted subtype must break the build at every consumer.
- 17Unchecked exceptions are for invariant violations — the panic analog. Checked exceptions do not cross seams; boundary adapters translate library exceptions into seam error types.
try-with-resourcesis MUST for anythingAutoCloseable; resource ownership is part of the seam's Javadoc.- Concurrency in the virtual-thread era: seams stay synchronous and blocking — virtual threads (final since 21) dissolved the function-coloring argument, so reactive types (
Mono/Flux/RxJava) are boundary adapters, never seam signatures. Ownership discipline: executors are created intry-with-resources(auto-close awaits completion — structured-by-construction untilStructuredTaskScopeleaves preview); nakednew Threadis banned;ThreadLocalis banned in cells (ScopedValueat the boundary where context propagation is truly needed).
5. specmark carrier
18Annotations — Java is the annotation culture, and the carrier exploits exactly the safe half of it:
19@SpecImplements(value = "<uri>", r = <N>) repeatable; one edge per annotation
@SpecDeviates(value = "<uri>", r = <N>, reason = "...") reason mandatory
@SpecVerifies(value = "<uri>", r = <N>) on test methods/classes
@SpecScope(value = "<uri>", r = <N>) in package-info.java — package-level inheritance
20Design: a tiny specmark artifact, compileOnly/provided scope, zero transitive dependencies, RetentionPolicy.CLASS — present in bytecode for T-syn/T-sem analysis, invisible to runtime reflection (aligned with the trunk's anti-reflection stance), and @Documented — the edges render in Javadoc, so the carrier doubles as the doc surface (the TS/Python simplification; the deliberate opposite of Go's hidden directives). The annotation's structure is compile-checked, which no comment carrier in the set can claim. No runtime processor exists or ever will. ≤3 edges per item or split.
6. Naming (R-020/R-021 bindings)
- 21Canonical cell class name is computed:
{Variant}{Seam}→SatDepSolver; linted (ArchUnit) against the manifest. - Forbidden in cells regardless of elegance — the Java theater list: reflection in domain code (
setAccessible,Class.forName, method handles on domain types); bytecode magic (ByteBuddy/cglib) outside infra; Lombok anywhere — banned not for license (MIT) but for surgery on javac internals that blinds A3 tooling, and because records/sealed types closed its legitimate niches; exceptions as control flow; serialization magic (Serializableon domain types); annotation processors generating domain logic (infra-only); God utility classes; builder ceremonies where a record with defaults suffices.
7. Replacement protocol (R-040 binding)
22A cell with replaces= ships a differential oracle: jqwik/QuickTheories properties driving both cells through the seam, asserting agreement modulo a documented divergence list, @SpecVerifies-tagged. Golden files follow the promotion protocol (CI never updates). In-source xfail twin — Java can have one: the profile ships a ~20-line JUnit 5 extension, @KnownFailing("debt-id"), with strict semantics — the annotated test failing is green, the test passing fails the run and forces promotion. @Disabled on known-failing tests is banned (it hides both regressions and healings — Go's t.Skip problem, here solvable). Mockito is banned in cell tests — reflection-based mocking is the vi.mock/mock.patch/gomock of Java, and capability injection makes it unnecessary; boundary tests may use it (MIT, fine there).
8. Risk table (what conform must cover for Java)
| Footgun | Rule | Tier |
|---|---|---|
static { } side effects / mutable static state in a cell |
§1 | T-syn |
System.getenv / ambient Clock/Random/HttpClient in a cell |
§1 | T-syn |
ServiceLoader registration for a cell |
§1 | T-syn |
reflection (setAccessible, Class.forName) on domain types |
§6 | T-syn |
| Lombok on the classpath | §6 | T-lex (build) |
| checked exception in a seam signature; expected failure thrown unchecked across a seam | §4 | T-syn |
default arm on a sealed-type switch |
§4 | T-syn |
Optional as field/parameter; undeclared null crossing a seam |
§2 | T-sem (NullAway) |
naked new Thread; executor outside try-with-resources; ThreadLocal in a cell |
§4 | T-syn |
| reactive type in a seam signature | §4 | T-syn |
Mockito in a cell test; @Disabled on a known-failing test |
§7 | T-syn |
| sibling-cell dependency; cell depended on outside the registry | R-002 | ArchUnit |
bare/over-broad @SuppressWarnings; stale suppression |
§0 | T-lex + sweep |
| Jackson/persistence annotations on domain types | §0 | T-syn |
| flag read outside the registry | R-001 | T-syn |
| public export without own/inherited spec edge | PROP-014 §3.2-6 | T-syn + index |
9. Doc layer
24Javadoc on every exported element: behavior, the seam's error codes with their REQ URIs, nullness beyond the annotations, thread-safety and blocking characteristics, resource ownership. The @Documented specmark edges render alongside — provenance is visible where readers already look. Spec stays thin; duplication between Javadoc and spec is a defect on the spec side.
25First carrier note. No Java exists in vibevm; no carrier is designated. Trunk and overlays ship genre-complete and unexercised under the carrier-relative house clause: rules remain DRAFT until a first Java carrier exists, and at its first milestone any rule without a conform check (or explicit WISH mark) is removed rather than carried as aspiration.