Java StringTemplates and logging, or why f-strings are not enough
Introduction
Java will soon get StringTemplates. At first sight, they may appear similar to f-strings in other programming languages, in that you can use them for string interpolation. They are different, however, in being way more flexible. They support custom Processors that can turn a StringTemplate into an object of any type1.
This flexibility enables implementations like a safe SQL Processor protecting against SQL injection attacks:
@Rpc
void logVehicle(LogVehicleRequest req) {
var plateNumber = req.getPlateNumber();
var speedKph = req.getSpeedKph();
var speedCameraId = req.getCameraId();
PreparedStatement query =
SQL."""INSERT INTO TABLICE (plateNum, speedKph, speedCameraId)
VALUES ('\{ plateNumber }', '\{ speedKph }', '\{ speedCameraId }');""";
query.executeQuery();
}
In this example:
SQL
is a custom string template processor performing appropriate arguments validation, and returning a PreparedStatement rather than a String."""INSERT INTO TABLICE [...]"""
is a template string.plateNumber
,speedKph
,speedCameraId
are arguments for the processor.

An f-string equivalent looks like this:
FMT."Average latency \{ latencyMs } ± \{ stdDevMs } ms"
Many people found the feature controversial, with every post about it on social media eliciting strong reactions. To many it looked “ugly”, “verbose”, “a mistake”, “over-engineered”; some questioned the motivation.
I didn’t have a strong opinion — until I wondered how StringTemplates would work in one of the most compelling and common use-cases — logging.
TL;DR: Perfectly: StringTemplates enable safe, efficient, powerful and ergonomic APIs.
Anatomy of logging APIs
A typical Java logging API looks like this (omitting the log level):
void log(String messagePattern, Object… formatArgs)
Let’s explore a few requirements that led many libraries in different languages to such signatures: performance and arguments processing.
First, logging can have a massive cost2. Logging libraries are designed to have little overhead, and avoid redundant work. For example, they:
Do not construct log messages if logging is disabled at the corresponding log level3:
No log message rendering
No potentially expensive conversion of format arguments to strings
No redundant I/O
Can implement advanced compression, not rendering log messages on the producer side at all. Uber got an incredible 169x compression rate using Compressed Log Processing (CLP) — a logging-specific compression algorithm.
Provide many overloads to avoid overheads from boxing of Java primitive types, and from var-arg array creation (see a post on the logging API anatomy from Flogger — Google’s Fluent Logger)4.
Can support rate limiting, either per message type, or per message type and a set of keys — again without rendering the entire message each time
Many more: lazy arguments creation, depth-limited call stack logging, etc.
Second, logging libraries need to process format arguments:
Pre-processing, for example, to:
Detect classes or fields with data privacy annotations, and redact PII or customer data.
Convert the arguments to a different format (like in CLP example above). Then, an Instant becomes a dozen bytes; or a protocol buffers message stays as is.
Post-processing (after rendering to string), for example, to:
Detect and redact patterns of sensitive data (emails, credentials) when it’s not properly annotated.
Truncate individual fields rather than the rendered log message.
One of the drawbacks of such logging API is it being error-prone. With many parameters, it’s easy to forget to pass one, or to mix the order5.
log(StringTemplate)
?
With StringTemplates, we can define a logging API as:
void log(StringTemplate messageTemplate)
Calling code:
log(RAW."Processed \{ numProcessed } out of \{ operations.size() }")
RAW
is a standard template processor, returning a StringTemplate instance (for subsequent processing in the logging library) — no String rendering happens at the call site.
f-string vs StringTemplates
Let’s see how f-strings and StringTemplates satisfy the logging libraries requirements.
Not constructing the log message
f-strings are rendered before calling log method, hence you may pay the entire string construction costs even at disabled levels6.
Rate limiting
Rate limiting with f-strings would still require rendering the log message.
With TemplateStrings, rate-limited invocation will entirely avoid log message construction costs.
Format arguments processing
With f-strings, the library doesn’t get individual arguments — they are inaccessible.
With TemplateStrings, the library gets them as-is, enabling any of the features above.
Lazy arguments creation
With TemplateStrings, users can keep using lazy arguments evaluation:
log(RAW."Stats: \{ lazy(this::generateExpensiveReport) }”)
Overloads
With f-strings, the cost of passing a single string reference is irrelevant — as real-world use cases require passing the format arguments7.
Even though I didn’t benchmark TemplateStrings instantiation and passing them as a method call argument, I don’t see anything in the design of the feature that’d preclude it from becoming as fast as specialized calls:
TemplateString is an interface, hence OpenJDK/JVM can generate as many specialized implementations as present today in the logging APIs (either at the standard library (source) level, or during compilation, or at runtime)8.
JVM can also share these auto-generated classes across different format methods (e.g., log methods in various libraries, Preconditions.checkArgument from Guava, assertion libraries, etc.).
Once Project Valhalla lands, these auto-generated implementations can have an optimal, flat memory layout, becoming indistinguishable on the call stack from a bunch of parameters today.
Therefore, eventually, logging APIs will be able to drop all but a few overloads.
Bonus: can I implement Log4Shell with StringTemplates?
Oh yes oh yes! If your project requirements include downloading and executing code off the internet, and, incidentally, getting its own meme collection to help engineers cope with response to a max severity vulnerability, then StringTemplates can help with that!
Seriously though, it’s up to a StringTemplate.Processor implementation to provide features that are safe to use, and perform proper validation — StringTemplates are not a silver security bullet.
Conclusion
This example demonstrates that StringTemplates enable APIs that are:
Powerful: StringTemplate-based logger API can supports all the features that now require a separate format string and arguments.
Fast: it can get as fast as specialized calls.
Safe: thanks to access to arguments, Processors can perform appropriate validations.
Easy to use: as arguments appear at use site, it’s clear where each expression ends up.
Acknowledgements
Big thanks to Henry Clifford, Onufry Wojtaszczyk (@OnufryW) and Stanislav Tkach for reviewing earlier drafts.
See also
JEP for details: https://openjdk.org/jeps/8323333
Javadoc: StringTemplate (Java SE 21 & JDK 21)
Flogger pages on design, best practices, and benefits
Until TemplateStrings arrive, consider using ErrorProne’s
@FormatMethod
annotation in the strict formatting APIs to enable users to catch (some) errors. On top of that, it can also check “lenient” format strings, however, for now only for a few known methods likePreconditions.checkArgument
from Guava.Compressed Log Processing (CLP) — a technology for efficient log storage and processing, turning all your existing text logs in ad-hoc structured logs. It enabled incredible 169x savings in log storage, with search that didn’t require decompression: Reducing Logging Cost by Two Orders of Magnitude using CLP | Uber Blog.
A fascinating, entertaining, and fun talk from a group of Polish security engineers who reverse-engineered Newag train software, uncovering an alleged large-scale fraud and critical infrastructure sabotage. Sadly for Newag, StringTemplates wouldn’t have helped: Breaking "DRM" in Polish trains
In terms of compute, or storage, or I/O throughput. There is no shortage of blog posts about “logging overhead” or “cost of logging”, with scary stories of double-digit percentage compute overheads; or seven-figure storage bills. For example, Uber could not afford to log and retain messages at info level in some of their high-QPS / latency-sensitive services.
In languages with macro support (Rust, C++) logging APIs are often implemented as a set of macros. The logging macros are expanded to conditional statements, guarding string formatting, argument evaluation and processing, etc., with a log level check. See Abseil docs for example.
Hundreds! See Anatomy of a logging API and the Flogger API; or Log4J2 API — the latter not including overloads for primitives.
When I annotated an internal project-specific logging API with ErrorProne @FormatMethod
, I got several dozen errors — and that cannot include incorrect parameter order!
I use “may” as it’s conceivable that (JIT) compiler inlines your logging call, re-arranges the order of expression evaluation (i.e., evaluate first isEnabledAt(Level.FINE) before f-string evaluation), and then avoids the cost of f-string evaluation. However, JLS requires the argument evaluation to happen before the method invocation, hence if the evaluation produces side-effects, JIT can’t possibly optimize that.
The logging API in Python — a language that has supported f-strings for years — is another evidence of that. See also the original PEP, saying “The APIs are structured so that calls on the Logger APIs can be cheap when logging is disabled”.