How Java text blocks and string templates clean up multi-line strings, SQL, JSON, and HTML — with practical examples for Spring Boot and API clients.
String concatenation is one of those Java pain points that’s been there so long developers barely notice it anymore. You’re building a SQL query with three joins, a JSON payload for an external API, or an HTML fragment for a test fixture, and you’re writing "SELECT " + "u.id, u.name " + "FROM users u " across six lines, fighting escaped quotes and backslashes, losing all sense of what the string actually looks like. It works, but it’s genuinely difficult to read and maintain.
Text blocks, finalised in Java 15, and string templates, previewed in Java 21, are the language’s answer to this. They’re not exotic features — they’re straightforward quality-of-life improvements that clean up code you’re already writing. I’ve used text blocks extensively across Spring Boot backends and test setup code, and the readability improvement is immediate.
A text block is a multi-line string literal delimited by """. The opening """ must be followed by a newline. The closing """ determines the indentation baseline:
String sql = """
SELECT u.id, u.name, u.email
FROM users u
JOIN roles r ON r.user_id = u.id
WHERE u.active = true
AND r.name = 'ADMIN'
ORDER BY u.name
""";
This is exactly what you’d write if you were writing SQL directly. No concatenation, no escaped quotes, no \n at the end of each line. The indentation up to the closing """ is stripped automatically — the resulting string starts with SELECT, not with spaces.
The closing """ position controls indentation stripping. If you place it at column 8, Java strips 8 leading spaces from every line. Place it at column 0 and no stripping happens. Move it one space right to deliberately preserve one space of leading indentation.
Two escape sequences are specific to text blocks.
\ (line continuation) suppresses the newline at the end of a line. Useful for long strings that logically belong on one line but need to be wrapped for readability:
String errorMessage = """
The claim could not be processed because the applicant \
does not meet the eligibility criteria for this benefit type.\
""";
// Result: single line, no embedded newlines
\s (trailing space) prevents the compiler from stripping trailing whitespace from a line. Text blocks strip trailing spaces by default — which is usually what you want, but occasionally you need to preserve them (fixed-width output formats, for example):
String paddedLine = """
FIELD_NAME \s
""";
// The space before \s is preserved
JSON in test fixtures is where text blocks make the biggest immediate impact. Before:
String body = "{\"claimId\": \"CLM-001\", \"status\": \"APPROVED\", " +
"\"amount\": 250.00, \"currency\": \"GBP\"}";
After:
String body = """
{
"claimId": "CLM-001",
"status": "APPROVED",
"amount": 250.00,
"currency": "GBP"
}
""";
The second form is what you’d paste from a JSON editor. You can read it, validate it visually, and modify it without wrestling with escape sequences. In integration tests using MockMvc or WireMock, this is a substantial improvement:
mockServer.stubFor(post(urlEqualTo("/api/decisions"))
.withRequestBody(equalToJson("""
{
"claimId": "CLM-001",
"decision": "APPROVE",
"reason": "Eligibility criteria met"
}
"""))
.willReturn(aResponse().withStatus(200)));
Spring Data’s @Query annotation accepts text blocks directly — which means your JPQL and native queries can be formatted exactly as you’d write them in a SQL tool:
@Repository
public interface ClaimRepository extends JpaRepository<Claim, String> {
@Query(value = """
SELECT c.claim_id,
c.status,
c.submitted_at,
a.nino,
a.full_name
FROM claims c
JOIN applicants a ON a.id = c.applicant_id
WHERE c.assigned_caseworker_id = :caseworkerId
AND c.status IN ('SUBMITTED', 'ASSIGNED')
ORDER BY c.submitted_at ASC
""",
nativeQuery = true)
List<ClaimSummary> findActiveClaims(@Param("caseworkerId") String caseworkerId);
}
Complex queries that previously lived in a separate .sql file or were unreadable string concatenations now live alongside the method that uses them, formatted readably.
Test data setup often needs HTML or XML snippets. Text blocks make these tolerable:
String responseBody = """
<claimResponse>
<claimId>CLM-001</claimId>
<status>APPROVED</status>
<paymentAmount currency="GBP">250.00</paymentAmount>
</claimResponse>
""";
mockMvc.perform(get("/api/claims/CLM-001")
.accept(MediaType.APPLICATION_XML))
.andExpect(content().xml(responseBody));
String templates extend text blocks with embedded expression interpolation. They were introduced as a preview feature in Java 21 (JEP 430) using the STR template processor. The syntax embeds expressions directly in the string using \{expression}:
String claimId = "CLM-001";
String status = "APPROVED";
BigDecimal amount = new BigDecimal("250.00");
// STR template processor
String message = STR."""
Claim \{claimId} has been \{status.toLowerCase()}.
Payment of £\{amount} will be processed within 5 working days.
""";
The STR processor evaluates each \{...} expression and interpolates the result. Unlike string concatenation, the template reads as the output string looks — you don’t have to mentally reconstruct the result from a chain of + operators.
Template processors are not limited to STR. The API allows custom processors for specific use cases — most importantly, safe SQL construction via a hypothetical SQL processor that would parameterise expressions rather than interpolating them as raw strings. The design explicitly prevents SQL injection by making the unsafe path (string concatenation) less convenient than the safe path (parameterised templates).
Text blocks are available from Java 15 onwards — they’re production-ready and widely adopted. String templates were a preview in Java 21 and 22, then withdrawn for redesign before Java 23. If you’re on Java 21 or 22 with preview features enabled you can use them, but for production code targeting current LTS releases (Java 21 LTS), treat string templates as a future feature and stick to text blocks with explicit concatenation for dynamic values:
// Safe, clear, available now on Java 15+
String notification = """
Claim %s has been %s.
Amount: £%.2f
Reference: %s
""".formatted(claimId, status.toLowerCase(), amount, reference);
String.formatted() (added in Java 15) works naturally with text blocks and avoids the verbosity of String.format() with a separate format string. It’s the pragmatic choice until string templates stabilise.
Text block usage isn’t limited to tests. In a Spring Boot application, constructing request bodies for external API calls is cleaner with text blocks:
@Service
@RequiredArgsConstructor
public class BetfairApiClient {
private final RestTemplate restTemplate;
public MarketCatalogue getMarketCatalogue(String marketId) {
String requestBody = """
{
"filter": {
"marketIds": ["%s"]
},
"marketProjection": [
"MARKET_START_TIME",
"RUNNER_METADATA",
"RUNNER_DESCRIPTION"
],
"maxResults": "1"
}
""".formatted(marketId);
return restTemplate.postForObject(
"/betting/rest/v1/en/navigation/menu.json",
requestBody,
MarketCatalogue.class);
}
}
This is more maintainable than string concatenation and more appropriate than a separate template file for a single-purpose request body. The formatted() call handles the variable substitution without introducing template engine dependencies.
Text blocks are one of those Java improvements that, once you start using them, make you reluctant to go back. The code is simply easier to read — and in a production system where the next developer (or future you) needs to understand what a query or payload does in seconds, that matters more than it might seem.
If you’re modernising a Java codebase or building new services on Java 21 and want an engineer who cares about code quality as much as delivery speed, get in touch.