name: frl-coding-standard
description: Coding standards for all Java code in fdb-record-layer. Apply when writing or reviewing any Java code.
Some example usages:
"Review this class for coding standards"
"Write a new implementation of RecordCursor"
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.
throw new RecordCoreException("Found corrupt record: " + record.getPrimaryKey());
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.
throw new RelationalException("Transaction rolled back", ErrorCode.SERIALIZATION_FAILURE);
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:
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.
LOGGER.info("Unable to open file: " + filename);
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.