# Frl Coding Standard

> All Java, Gradle, and property files MUST end with a newline character.

- Skill: `foundationdb/frl-coding-standard` (Agent Skill)
- Install (CLI): `npx skillmds@latest add foundationdb/frl-coding-standard`
- Raw SKILL.md: https://api.skillmd.com/api/skills/foundationdb/frl-coding-standard/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: foundationdb (https://skillmd.com/u/foundationdb)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/foundationdb/frl-coding-standard

---


All Java, Gradle, and property files MUST end with a newline character.

Target Java 17 language level, compiled with JDK 21.

## Exceptions

The two layers use different exception systems. **Know which layer you are in.**

### fdb-record-layer-* (record layer)

Exceptions extend `RecordCoreException`, which extends `LoggableException` (unchecked).

- The exception message MUST be a **static string** — no string interpolation or variables.
- Use `.addLogInfo(LogMessageKeys.KEY, value)` to attach context. Include as much as needed
  to diagnose the error: subspace, primary key, record type, index name, etc.

```java
// Wrong
throw new RecordCoreException("Found corrupt record: " + record.getPrimaryKey());

// Correct
throw new RecordCoreException("Found corrupt record")
    .addLogInfo(LogMessageKeys.PRIMARY_KEY, record.getPrimaryKey())
    .addLogInfo(LogMessageKeys.RECORD_TYPE, record.getRecordType());
```

### fdb-relational-* (relational / SQL layer)

Exceptions are `RelationalException` (checked, extends `Exception`). Always pair with an
`ErrorCode`, which maps to a SQLSTATE-standard 5-digit code.

- Message must still be a **static string**.
- Attach context with `.addContext("key", value)` — this travels with the exception for
  logging and is preserved when converting to `SQLException`.

```java
// Minimal form
throw new RelationalException("Transaction rolled back", ErrorCode.SERIALIZATION_FAILURE);

// With context
throw new RelationalException("Unsupported type in column", ErrorCode.CANNOT_CONVERT_TYPE)
    .addContext("columnName", colName)
    .addContext("typeName", typeName);
```

At JDBC interface boundaries (where the signature throws `SQLException`), convert via
`.toSqlException()`, which produces a `ContextualSQLException` carrying the `ErrorCode`
as the SQLState and the context map for logging:

```java
throw new RelationalException("Element is not of STRUCT type", ErrorCode.CANNOT_CONVERT_TYPE)
    .toSqlException();
```

Specialized subclasses (`InvalidColumnReferenceException`, `InvalidTypeException`) exist for
common error cases — prefer them when they fit. `InternalErrorException` is deprecated; use
`new RelationalException("...", ErrorCode.INTERNAL_ERROR)` directly.

`UncheckedRelationalException` wraps a `RelationalException` as a `RuntimeException` for
contexts where checked exceptions cannot be thrown (e.g., inside lambdas). Unwrap with
`.unwrap()` to recover the original `RelationalException`.

## Logging

Same principle as exceptions — structured, static messages:

- Always use `KeyValueLogMessage.of("static message", key, value, ...)`.
- The message text must be a static string. Variables go in key/value pairs.

```java
// Wrong
LOGGER.info("Unable to open file: " + filename);

// Correct
LOGGER.info(KeyValueLogMessage.of("Unable to open file", LogMessageKeys.FILENAME, filename));
```

## Futures (async code)

The `fdb-record-layer-*` and lower-level parts of `fdb-relational-core` are fully asynchronous
via `CompletableFuture`. The JDBC-facing relational API presents a synchronous surface, but
async code is common in the implementation beneath it.

- **Never** call `join()` or `get()` in production code. Use `context.asyncToSync()` or
  `database.asyncToSync()` instead — they enforce timeouts, record wait timers, and handle
  error wrapping.
- **Never** call any blocking method from inside a `CompletableFuture` completion lambda. The
  thread pool is finite; blocking inside a future can deadlock the entire system.
- **Avoid** `thenApplyAsync()` / `thenComposeAsync()` unless the lambda does significant CPU
  work. These enqueue a new task on the executor for no benefit in the common case. When you
  do need them, always pass an explicit `Executor`.
- **Always pass an `Executor`** to any `AsyncUtil` or `MoreAsyncUtil` method that accepts one
  (e.g. `AsyncUtil.whileTrue()`). Never use the overload without an executor.

## Checkstyle-banned imports

The authoritative list is in `gradle/check.gradle` (`BANNED_IMPORTS`).

## Inline comments

Describe **intent**, not mechanics. Explain the *why* — a hidden constraint, a non-obvious
invariant, a workaround for a specific FDB behavior. If removing the comment would not confuse
a future reader, don't write it.

## Javadoc

Add Javadoc for public APIs. It is not necessary for private methods unless the logic is
genuinely non-obvious.

