How to Use Kotlin String Templates for Complex Formatting with Multiple Variables and Expressions

Kotlin string templates allow you to embed variables and arbitrary expressions directly inside string literals using $variable or ${expression} syntax, with the compiler automatically optimizing the bytecode to use StringBuilder for complex multi-part templates.

The JetBrains/kotlin repository implements string templates as first-class PSI (Programmatic Structure Interface) elements. When you write a template like "Hello, $name", the compiler represents it as a KtStringTemplateExpression containing an ordered list of entry objects that later translate into efficient concatenation operations or StringBuilder sequences.

Understanding the Kotlin String Template Architecture

At the core of every template lies the KtStringTemplateExpression class located in compiler/psi/psi-api/src/org/jetbrains/kotlin/psi/KtStringTemplateExpression.java. This PSI node acts as a container for individual template entries, each representing a distinct segment of your string.

The compiler decomposes every template into a sequence of KtStringTemplateEntry subclasses:

  • KtLiteralStringTemplateEntry – Represents plain text fragments between interpolations
  • KtSimpleNameStringTemplateEntry – Handles $variable syntax for simple identifiers
  • KtBlockStringTemplateEntry – Manages ${expression} blocks containing arbitrary Kotlin expressions
  • KtEscapeStringTemplateEntry – Processes escaped dollar signs ($$) and newline handling

Each entry type serves a specific role in the compilation pipeline, ensuring that the resulting bytecode maintains both correctness and performance.

Entry Types and Their Use Cases

Simple Variable Insertion

For direct variable references, use the $identifier syntax. This creates a KtSimpleNameStringTemplateEntry, which is the most efficient form of interpolation.

val user = "Alice"
val greeting = "Welcome, $user!"  // KtSimpleNameStringTemplateEntry

The compiler treats this as a direct reference without additional expression evaluation overhead.

Expression Blocks

When you need to embed complex logic, method calls, or operators, use the ${expression} syntax. This generates a KtBlockStringTemplateEntry as defined in compiler/psi/psi-api/src/org/jetbrains/kotlin/psi/KtBlockStringTemplateEntry.java.

val score = 95.5
val result = "Final score: ${score.format("%.1f")}%"  // KtBlockStringTemplateEntry

The braces ensure the entire expression evaluates as a single unit before string conversion.

Literal Text and Escaping

Raw text segments become KtLiteralStringTemplateEntry instances, while literal dollar signs require KtEscapeStringTemplateEntry via the $$ syntax.

val price = 100
val display = "Price: $$price"  // Outputs: Price: $100

In this example, $$ creates an escape entry emitting a single $, while $price creates a simple entry for the variable.

Compiler Optimization and Bytecode Generation

The Kotlin compiler optimizes template compilation through two primary strategies. For short templates with few interpolations, it generates chained String.plus calls. For longer or multi-line templates, it automatically switches to StringBuilder operations.

The StringTemplateExpressionManipulator class in compiler/psi/psi-impl/src/org/jetbrains/kotlin/psi/psiUtil/StringTemplateExpressionManipulator.kt handles the underlying manipulation logic used by the IDE for refactoring and in-place edits. This utility ensures that when you modify templates programmatically, the entry boundaries remain consistent.

You can verify the optimization path by examining the bytecode: templates exceeding approximately ten concatenations trigger the StringBuilder path automatically, eliminating manual performance tuning in most cases.

Best Practices for Complex Kotlin String Templates

Simple identifiers – Use $name instead of ${name} when referencing simple variables to avoid unnecessary KtBlockStringTemplateEntry overhead.

Multi-line formatting – Leverage raw strings ("""...""") combined with trimMargin() for readable indentation:

val report = """
    |User: $userName
    |Score: ${score.format("%.2f")}
    |Items: ${items.joinToString(", ")}
    |Cost: $$${price}
""".trimMargin()

This pattern handles complex formatting while preserving source code alignment.

Keyword identifiers – When interpolating identifiers that match Kotlin keywords or contain special characters, the compiler automatically handles backticks within block entries:

val `if` = "condition"
val text = "Value: ${`if`}"  // Block entry handles backtick-escaped identifiers

Custom interpolation prefixes – For DSLs requiring literal dollar signs before expressions, use the multi-dollar factory method createMultiDollarStringTemplate in compiler/psi/psi-api/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt (lines 524-533), which supports prefixLength parameters.

Programmatic Template Creation with KtPsiFactory

When generating code programmatically, use KtPsiFactory to construct template PSI nodes. The factory provides three key methods:

  • createStringTemplate – Builds standard double-quoted templates
  • createRawStringTemplate – Creates triple-quoted raw templates
  • createMultiDollarStringTemplate – Generates templates with custom interpolation prefixes for DSL scenarios

Located in compiler/psi/psi-api/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt (lines 511-533), these factory methods ensure proper entry ordering and prefix handling when constructing KtStringTemplateExpression instances dynamically.

Common Pitfalls to Avoid

Unnecessary braces – Avoid ${variable} when $variable suffices. The simple form generates more efficient PSI structures.

Raw string escaping – Remember that $ remains a template marker even inside """...""". Use $$ to emit literal dollar signs in raw strings.

Nested expressions – Do not split complex expressions into multiple interpolations. Combine them into a single ${...} block to ensure atomic evaluation and cleaner bytecode.

Performance micro-optimization – Trust the compiler's automatic StringBuilder selection. Only manually implement StringBuilder chains when profiling reveals bottlenecks in tight loops.

Summary

  • Kotlin string templates decompose into KtStringTemplateEntry subclasses, with KtSimpleNameStringTemplateEntry for variables and KtBlockStringTemplateEntry for expressions.
  • The compiler automatically optimizes templates to use StringBuilder when exceeding simple concatenation thresholds.
  • Use $variable for simple identifiers and ${expression} for complex logic or method calls.
  • Escape literal dollar signs with $$, particularly in raw multi-line strings.
  • Generate templates programmatically using KtPsiFactory methods like createStringTemplate and createRawStringTemplate.

Frequently Asked Questions

What is the performance cost of using kotlin string templates?

Kotlin string templates compile to the same bytecode as manual StringBuilder operations or concatenations. The compiler automatically selects the most efficient strategy based on template complexity, resulting in zero runtime overhead compared to equivalent manual string construction.

How do I escape dollar signs in raw string templates?

Use the $$ syntax, which creates a KtEscapeStringTemplateEntry. Even inside triple-quoted raw strings ("""..."""), single $ characters trigger template evaluation, making $$ necessary for emitting literal dollar signs.

Can I use kotlin string templates with DSLs that require literal dollar signs?

Yes. Use the createMultiDollarStringTemplate method from KtPsiFactory with a custom prefixLength parameter, or manually construct templates using $$${expression} patterns where the first $$ emits a literal $ followed by the interpolation block.

When should I use curly braces vs simple $ syntax?

Use simple $ syntax only for single identifiers without special characters. Use ${} braces for expressions containing operators, method calls, properties with complex accessors, or identifiers that require backtick escaping. The braces ensure proper expression boundaries and prevent parsing ambiguities.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →