Formatting Decisions & Configuration Options¶
This document is a user-facing explainer with examples. The normative formatter contract is
docs/canonical-formatting-rules.md.
This document lists the key formatting decision points for the Prince of Space formatter. Each section describes a decision area, the options, what other formatters do, and our proposed default.
Part 1: Configuration Options (The Knobs)¶
1. Indentation: Tabs vs Spaces¶
| Option | Used By |
|---|---|
| Spaces (4) | Java convention since 1999; Oracle style guide; IntelliJ default |
| Spaces (2) | Google Java Style; google-java-format default |
| Tabs | A minority of Java projects; more common in Go, C |
Config: indentStyle: spaces|tabs, indentSize: <number> (number of spaces or number of tabs per indent level)
Default: spaces, 4
With spaces, indentSize is how many space characters make up one indent step. With tabs, indentSize is how many tab characters (\t) make up one indent step (often 1).
2. Line Length¶
A single target line width. The formatter tries to keep lines at or below this length by wrapping when necessary. Some constructs cannot be wrapped — such as very long string literals, generated data files, or deeply nested expressions with no safe break point — and will exceed lineLength when no wrap point exists. Comment and text-block content are preserved verbatim.
This is similar to how Prettier treats printWidth as a guide rather than a hard wall.
// Line is 125 chars, lineLength is 120. Wrapping produces clean output, so we wrap:
var result = someService.processRequest(
requestId, userName, Optional.of(defaultConfig), additionalParams);
Config: lineLength: <number>
Default: 120
3. Continuation Indent¶
When a statement wraps to the next line, the continuation is always indented by 2 * indentSize. This is not configurable — it follows the Oracle/IntelliJ convention (indent=4 → continuation=8, indent=2 → continuation=4) and guarantees that wrapped parameters are visually distinct from the method body.
The continuation indent is additive over the enclosing block indent (not an absolute left margin from the statement start).
// indentSize = 4, continuation = 8
if (ready) {
return veryLongCondition &&
nextPart; // 12 spaces (4 block + 8 continuation)
}
public void process(
String name, // 8 spaces continuation — clearly not body
int age) {
doSomething(); // 4 spaces body indent
}
Method-chain exception. Wrapped method-chain segments use a single indentSize step instead of 2 * indentSize. Each chain segment begins with a ., which is its own visual delimiter, so the deeper continuation indent is not needed and only adds excess depth in nested chains. See "Method Chaining" in Part 3 and TDR-015 for rationale.
4. Line Wrapping Strategy¶
When wrapping is triggered, how do we distribute elements across lines?
| Strategy | Name | Description |
|---|---|---|
| Keep as much on one line as possible | wide |
Only wrap what's needed to stay within limits |
| If wrapping needed, one element per line | narrow |
Every element gets its own line |
| Fit on one line or go full one-per-line | balanced |
All-or-nothing: fits on one line, or each element gets its own line |
Config: wrapStyle: wide|narrow|balanced
Default: balanced
The balanced strategy (Prettier's approach) avoids the messy middle ground where some args are on one line and others on the next. Either it all fits, or each gets its own line.
Uniform “list-like” shape: the same wrapStyle semantics apply everywhere the formatter breaks a delimited list (method/constructor arguments, implements / extends / permits lists, array initializers, for loop headers, try-with-resources, multi-case labels, generic type parameters/arguments, and so on)—except enum constants, which are always one-per-line regardless of wrap style. In short for those lists: wide greedily packs elements until a line is full, then continues; balanced and narrow** use the fit-or-tall pattern—if the whole list does not fit on the remainder of the line, each element is placed on its own continuation line (with narrow sometimes using deeper continuation indent where the engine already does so for other constructs).
5. Multi-Line Parameter Closing Style¶
When method parameters or arguments are wrapped across multiple lines, should the closing parenthesis go on its own line?
// closingParenOnNewLine: true (default)
public void process(
String name,
int age,
boolean active
) {
// ...
}
doSomething(
name,
age,
active
);
// closingParenOnNewLine: false
public void process(
String name,
int age,
boolean active) {
// ...
}
doSomething(
name,
age,
active);
This applies consistently to both method parameter declarations and method call arguments.
Nested wrapped-call behavior follows opener placement:
// Co-line nested openers, closingParenOnNewLine=true:
methodA(methodB(
aaaaaaa,
bbbbbbb,
ccccccc
));
// Line-separated nested openers, closingParenOnNewLine=true:
methodA(
methodB(
aaaaaaa,
bbbbbbb,
ccccccc
)
);
// closingParenOnNewLine=false keeps closers inline on the last content line:
methodA(
methodB(
aaaaaaa,
bbbbbbb,
ccccccc));
For a single wrapped non-chain argument, the formatter breaks before the argument for readability:
Scoped method-chain receivers remain the carve-out and may stay inline after ( when that preserves Rule 7 chain layout.
Wrapped type clauses: When implements, extends, or permits lists wrap across lines, the { that begins the class/interface/record body uses the same idea: with closingParenOnNewLine=true, the { is typically alone on the line after the last type; with false, { stays on the same line as the last type (K&R style). There is no ) in a type clause, but the config name reflects the shared “closing delimiter” behavior.
Since continuation indent is always 2 * indentSize, wrapped parameters are always visually distinct from the method body regardless of the closingParenOnNewLine setting.
Config: closingParenOnNewLine: true|false
Default: true
6. Trailing Commas¶
In Java, trailing commas are valid in enum constants and array initializers. Adding them in multi-line contexts produces cleaner diffs.
// trailingCommas: true
enum Status {
ACTIVE,
INACTIVE,
PENDING,
}
String[] names = {
"Alice",
"Bob",
"Charlie",
};
// trailingCommas: false (default)
enum Status {
ACTIVE,
INACTIVE,
PENDING
}
Note: Java only supports trailing commas in enum constants and array initializers. This option only affects those contexts.
Config: trailingCommas: true|false
Default: false
7. Java Language Level¶
The formatter parses input according to a configured Java release. This determines which syntax JavaParser will accept, such as records, sealed types, switch expressions, text blocks, and newer language features supported by the bundled parser version. It does not introduce extra style knobs; it only controls the accepted source-language surface.
Config: javaLanguageLevel: JavaLanguageLevel.of(<release>) in the API, or --java-version <release> in the CLI
Default: JavaLanguageLevel.of(17)
Part 2: Configuration Summary¶
| Option | Type | Default | Description |
|---|---|---|---|
indentStyle |
spaces | tabs |
spaces |
Use tabs or spaces for indentation |
indentSize |
integer | 4 |
Number of spaces or tabs per indent level |
lineLength |
integer | 120 |
Target line width; wrapping is triggered here |
wrapStyle |
wide | narrow | balanced |
balanced |
How to handle line wrapping |
closingParenOnNewLine |
boolean | true |
Whether closing ) goes on its own line in multi-line params/args |
trailingCommas |
boolean | false |
Add trailing commas in enum constants and array initializers |
javaLanguageLevel |
JavaLanguageLevel |
JavaLanguageLevel.of(17) |
Java syntax release accepted by the parser |
Continuation indent is always 2 * indentSize (e.g. 8 spaces with the default indentSize=4). This is not configurable.
Total: 7 options.
Part 3: Decided Formatting Behaviors (Not Configurable)¶
Method Chaining (Fluent APIs / Builders / Streams)¶
When a chain wraps (line length exceeded, or a lambda-heavy chain forces wrapping), each chained call is placed on its own line with a leading dot (same idea as Kotlin's fluent style and Prettier's typical JS/TS chains). The receiver is alone on the first line; every .method(...) after it starts a continuation line. Each continuation line is indented by adding one indentSize step on top of the active enclosing indent (not the 2 * indentSize continuation indent used for delimited list continuations, and not dot-aligned into the horizon).
// Multi-segment chain (two or more .method() links after the receiver)
// indentSize = 4 → chain segments at 8 spaces (block 4 + chain 4)
var result = list
.stream()
.filter(x -> x.isActive())
.map(x -> x.getName())
.collect(Collectors.toList());
Why a single indentSize step? A wrapped method chain is already visually self-delimiting: every segment begins with a leading ., so a reader can pick out the chain at a glance even without extra indentation. Using one indent step instead of two keeps deeply nested chains (streams of streams, builder chains inside other chains) from drifting far to the right.
Block lambdas inside a wrapped chain segment continue to use ordinary block indent (indentSize) for the lambda body, measured from the chain-segment's column:
CompletableFuture
.supplyAsync(() -> {
loadData();
return processData();
}, executorService)
.thenApply(result -> transformResult(result));
Wrapped chain as an operand of a binary chain. When a wrapped method chain is itself an operand inside a wrapped binary/logical chain (Rule 6), the chain segments are pushed one extra indentSize past the operator's continuation column, so the chain stays visually distinct from the operator that introduces it:
return items != null
&& items
.stream() // chain dots one step past `&&`
.filter(s -> !s.isBlank())
.anyMatch(s -> s.contains(query));
Single-segment chain: If there is only one method call after a simple receiver (a name, this, super, or field access such as obj.field), it stays on one line with the receiver so trivial calls do not add an extra line:
If the receiver is not "simple" (e.g. a parenthesized or nested expression), the lone .method() still begins on the next continuation line so layout stays consistent.
Rationale: Leading-dot chains are easy to scan, produce one method per line in diffs when the chain changes, and align with common practice in Kotlin and in Prettier-style formatters. The single-call exception matches typical Java usage for foo.bar() and items.stream() without needless vertical sprawl. See TDR-015 for the chain-indent decision history.
Lambda Expressions¶
// Short lambda - inline
list.forEach(x -> System.out.println(x));
// Block lambda - brace on same line as arrow
list.forEach(x -> {
process(x);
log(x);
});
// Lambda as method argument - opens inline, never pushes paren to new line
executor.submit(() -> {
doWork();
cleanup();
});
// Trailing block lambda with preceding args - lambda header stays on the call line,
// body wraps with block indent, "})" closes inline at the call's indent column
// regardless of closingParenOnNewLine.
String result = computeWithDefault(thisIsAVeryLongArgument, () -> {
return loadDefault();
});
// Single-arg call where the argument is an expression-bodied lambda - the lambda header
// "s ->" stays on the call line and the body wraps as a chain receiver.
items.stream()
.map(s -> s
.toLowerCase()
.chars()
.mapToObj(ch -> String.valueOf((char) ch))
.collect(Collectors.joining())
.trim());
// Trailing expression lambda in a multi-arg call.
return computeFromInputData(thisIsAReasonablyLongArgument, value -> value
.transform()
.normalize()
.toString());
Lambda arguments should NOT cause the opening paren to wrap to a new line (this is the google-java-format mistake that everyone dislikes). When the last argument of a wrapped call is any lambda — block- or expression-bodied — the lambda header (() -> {, (a, b) -> {, s ->, value ->, …) is kept on the call line and the lambda body wraps according to its own rules: a block body uses its own block indent, an expression body wraps via the receiver chain or other inner wrap mechanic. The closing ) always follows the lambda body inline, never on its own line. This matches palantir-java-format / Prettier / ktlint and applies even if the resulting opener line slightly exceeds lineLength. See TDR-021.
Ternary Expressions¶
// Fits on one line
var x = condition ? "yes" : "no";
// Doesn't fit - operator at start of continuation line
var result = someVeryLongCondition
? computeThisValue()
: computeThatValue();
Binary Operator Wrapping¶
// Operator at start of continuation line
boolean result = isVeryLongConditionA()
&& isVeryLongConditionB()
&& isVeryLongConditionC();
Operators at line start makes logical structure visible from the left margin.
String concatenation (+) follows the same wrap-style policy as other binary operators:
balanced/narrow use one operand per continuation line, while wide greedily packs.
Forced Braces¶
Braces are always required around if, else, for, while, do bodies — even single-statement bodies.
// Input (no braces)
if (condition) doSomething();
// Output (braces added)
if (condition) {
doSomething();
}
Brace Placement¶
K&R style (opening brace on same line). This is the overwhelming Java convention. Not configurable.
Blank Lines¶
- One blank line between methods
- One blank line between field groups and methods
- No more than one consecutive blank line anywhere (collapse multiples)
- No blank line after opening brace or before closing brace
- Preserve single blank lines within method bodies (developer intent)
Annotation Placement¶
// Declaration annotations: own line, one per line
@Override
@Nonnull
public String toString() {
// Parameter annotations: inline (preserved as-is)
public void process(@Nonnull String name, @Valid Request request) {
// Type-use annotations (e.g., JSpecify @Nullable): position preserved
// We do NOT move these. If a developer writes:
public @Nullable String result() { ... }
// it stays exactly there. We respect type-use annotation placement.
Type-use annotations (like JSpecify's @Nullable) are placed directly adjacent to the type they annotate. The formatter preserves this position and does not reorder annotations relative to modifiers. This is critical for JSpecify/nullness correctness where @Nullable must be next to the type, not before modifiers.
Try-with-resources¶
// Single resource - one line
try (var stream = Files.lines(path)) {
// Multiple resources - each declaration aligned vertically
try (var input = new FileInputStream(src);
var output = new FileOutputStream(dest)) {
When multiple resources are declared, var (or type) declarations are aligned to the same column. The closing ) follows closingParenOnNewLine setting (implemented in PrincePrettyPrinterVisitor.visit(TryStmt) when there is more than one resource).
Array and Collection Initializers¶
Governed by wrapStyle. Same fit-or-tall logic as method arguments. Trailing commas governed by trailingCommas setting.
// Fits on one line
int[] values = {1, 2, 3, 4, 5};
// Doesn't fit - one per line
String[] names = {
"Alice",
"Bob",
"Charlie"
};
Enum Constants¶
Enum constants are always formatted one per line (the formatter never emits { RED, GREEN, BLUE } on a single header line), regardless of lineLength, wrapStyle, or constant count:
enum Color {
RED,
GREEN,
BLUE
}
enum Status {
ACTIVE,
INACTIVE,
PENDING,
DELETED
}
// Enum constants with constructors or anonymous bodies still one constant per line
enum Planet {
EARTH(5.976e+24, 6.37814e6),
MARS(6.421e+23, 3.3972e6);
// ...
}
Import Organisation¶
Delegated to Spotless. The Prince of Space formatter does not handle import sorting, grouping, or removal. This is Spotless's responsibility and can be configured independently.
Part 4: Options We Considered But Rejected¶
| Option | Why Rejected |
|---|---|
| Brace style (K&R vs Allman) | Java community has overwhelming consensus on K&R |
| Import order customization | Delegated to Spotless |
| Blank line customization | Our defaults match community standards |
| Operator position (start vs end of line) | Start-of-line is clearly more readable; no need for a knob |
| Single vs double quotes | Java doesn't have this choice |
| Force braces toggle | Always forcing braces is a safety/readability best practice |