Project Amber is the OpenJDK project that explores and delivers small, productivity-oriented Java language features. "Small" is relative: Amber gave Java var, switch expressions, text blocks, records, sealed classes and pattern matching, which together change how idiomatic Java models data. Each feature is modest on its own. The value comes from how they fit together, and from the fact that they arrived one release at a time, so most codebases have adopted some of them and not others.

This page is about Amber as a whole: how its features are shipped, what has been delivered and when, how the pieces compose, what is still in flight as of JDK 27, and how to plan adoption around the long-term-support release you run. Records and pattern matching each have a dedicated deep dive on this site; this page links to them rather than repeating them.

Advertisement

How an Amber feature ships

Amber features follow the JDK Enhancement Proposal (JEP) process, and most of them go through one or more preview rounds before they become permanent. A preview feature is fully specified and implemented, but disabled unless you opt in, and it may change or disappear in the next release. Each preview round gets its own JEP number, which is why a feature such as pattern matching for switch has a chain of four preview JEPs (406, 420, 427, 433) before its final JEP 441.

You opt in at compile time and at run time with --enable-preview, and the compiler insists that you also name the exact release:

# Compile and run code that uses a preview feature of the JDK you are on.
javac --release 27 --enable-preview -Xlint:preview src/Main.java -d out
java --enable-preview -cp out Main

# Single-file launch also needs the flag and the release.
java --enable-preview --source 27 Main.java

The operational consequence: a class file that uses preview features runs only on the exact JDK release that compiled it, so code compiled with preview features on JDK 26 will not run on JDK 27. Preview features suit experiments and internal tools, not libraries other teams depend on.

Preview rounds are also where designs change. String templates previewed in JDK 21 and 22 and were then withdrawn (JEP 465).

What Amber has delivered

The table lists the features whose final JEP has been delivered, with the release in which each became a permanent part of the language. Earlier preview JEPs are listed where they exist.

FeatureFinal JEPFinal inPreviews
Local-variable type inference (var)286JDK 10none
var in lambda parameters323JDK 11none
Switch expressions361JDK 14325, 354
Text blocks378JDK 15355, 368
Pattern matching for instanceof394JDK 16305, 375
Records395JDK 16359, 384
Sealed classes409JDK 17360, 397
Record patterns440JDK 21405, 432
Pattern matching for switch441JDK 21406, 420, 427, 433
Unnamed variables and patterns (_)456JDK 22443
Launch multi-file source-code programs458JDK 22none
Module import declarations511JDK 25476, 494
Compact source files and instance main methods512JDK 25445, 463, 477, 495
Flexible constructor bodies513JDK 25447, 482, 492

JDK 21 completed the data-oriented core; JDK 25 added the on-ramp and ergonomics features.

Advertisement

How the features compose

How Amber's features compose into data-oriented programmingRecords (JDK 16)transparent data carriersSealed types (JDK 17)closed set of alternatives= algebraic data typesinstanceof patterns (16)test + bind in one stepRecord patterns (21)deconstruct, nestswitch patterns (21)exhaustive over sealedSwitch expressions (14)yield a value, no fall-throughUnnamed _ (22)ignore what you don't needPrimitive patternspreview; 5th in JDK 27Compiler-checked data processingadd a case and every non-default switch that misses it fails to compileAround the core: var (10), text blocks (15), compact source files, module imports and flexible constructor bodies (25).Numbers are the JDK release in which each feature became final.
Records and sealed types define the shape of data; patterns take it apart; switch expressions turn the result into a value; the compiler checks coverage.

The central idea is what the Amber team calls data-oriented programming. A record says "this type is exactly these components", so the compiler knows how to construct and deconstruct it. A sealed interface says "these are all the alternatives", so the compiler knows the full set of cases. Together they are what functional languages call algebraic data types: a sum of products.

Patterns are the consumer side. A type pattern tests and binds in one step. A record pattern deconstructs a record into its components and can nest, so case Refunded(var id, var cents, var reason) replaces a cast and three accessor calls. A pattern switch over a sealed type needs no default branch because the compiler can prove every case is covered, and a switch expression must be exhaustive, so a missing case is a compile error rather than a run-time surprise.

The smaller features fill gaps: _ ignores components you don't need, and var keeps nested patterns readable.

Worked example: from instanceof chains to sealed records

Consider a payment service that applies events to a ledger. The Java 8 version uses an abstract class, hand-written value classes and an instanceof chain:

// Java 8 style: an open class hierarchy and a chain of instanceof checks.
abstract class PaymentEvent { }
final class Authorized extends PaymentEvent {
    private final String id; private final long cents;
    Authorized(String id, long cents) { this.id = id; this.cents = cents; }
    String id() { return id; } long cents() { return cents; }
    // equals, hashCode, toString written by hand (or forgotten)
}
final class Captured extends PaymentEvent { /* same boilerplate */ }
final class Refunded extends PaymentEvent { /* same boilerplate, plus a reason */ }

static long ledgerDelta(PaymentEvent e) {
    if (e instanceof Authorized) {
        return 0;
    } else if (e instanceof Captured) {
        return ((Captured) e).cents();
    } else if (e instanceof Refunded) {
        return -((Refunded) e).cents();
    }
    throw new IllegalStateException("unknown event " + e);   // found at run time, if ever
}

The value classes carry error-prone boilerplate, nothing stops a fourth subclass appearing elsewhere, and when one does the method still compiles and throws at run time.

The same logic with Amber features:

// Java 21+ style: a sealed interface, records, and an exhaustive pattern switch.
sealed interface PaymentEvent permits Authorized, Captured, Refunded { }
record Authorized(String id, long cents) implements PaymentEvent { }
record Captured(String id, long cents) implements PaymentEvent { }
record Refunded(String id, long cents, String reason) implements PaymentEvent {
    Refunded {                                   // compact constructor: validate once
        if (cents <= 0) throw new IllegalArgumentException("refund must be positive");
    }
}

static long ledgerDelta(PaymentEvent e) {
    return switch (e) {                          // no default: the compiler proves coverage
        case Authorized _                -> 0;   // unnamed pattern, JDK 22+
        case Captured(var id, var cents) -> cents;
        case Refunded(_, var cents, _)   -> -cents;
    };
}

The records remove the boilerplate and put validation in one place, the compact constructor. The sealed interface closes the hierarchy: permits lists every allowed implementation, and each must be final, sealed or non-sealed (records are implicitly final). The switch has no default, so when someone adds record Chargeback(...) to the permits list, every switch that does not handle it stops compiling. That is the real payoff: a change to the data model produces a to-do list from the compiler.

Do the migration in steps. First convert value classes to records where their semantics really are "just these fields"; the records deep dive covers when that is and isn't true. Then seal the hierarchy. Then convert dispatch sites to pattern switches, removing default branches as you go, because a default defeats the exhaustiveness check.

Exhaustiveness across separate compilation

The compile-time guarantee covers code compiled together. Java still allows separate compilation, so it is worth knowing what happens when it breaks down. If a library adds a permitted subtype and your code is not recompiled, an exhaustive pattern switch that meets the new type at run time throws MatchException. The compiler inserts that as a synthetic default for exactly this case.

So keep a sealed hierarchy and the switches over it on the same release train, and when a sealed type is a published API, treat adding a permitted subtype as a breaking change.

Null is the other edge. A pattern switch throws NullPointerException on a null selector unless it has an explicit case null. The pattern matching deep dive covers dominance ordering, guards with when and the precise coverage rules.

The everyday features: var, text blocks, switch expressions

var infers a local variable's static type from its initializer; it is not dynamic typing, and it is not allowed for fields, parameters or return types. Use it when the type is visible on the right-hand side.

Text blocks, delimited by """, strip incidental indentation and trailing whitespace; \s keeps a significant space and a trailing backslash joins lines. They make embedded SQL readable but do nothing about injection, so keep bind parameters.

Switch expressions use arrow labels, never fall through, return a value (yield from a block) and must be exhaustive, which makes them safer than the statement form even over enums.

The JDK 22 to 25 wave

Four later features target different audiences. Unnamed variables and patterns (JDK 22) apply _ to unused lambda parameters, catch parameters, loop variables and pattern components. Compact source files and instance main methods (JDK 25) let a beginner or a script author write a file without a class declaration, with a main that need not be static or take arguments; such files import the java.base packages automatically and can use the java.lang.IO console helper. Module import declarations import every public type exported by a module in one line. Flexible constructor bodies allow statements before super(...) or this(...), as long as they do not use the object under construction.

// Hello.java - a compact source file (JDK 25): no class declaration, instance main.
void main() {
    String name = IO.readln("Name? ");
    IO.println("Hello, " + name);
}

// --- a separate file: module import (JDK 25), all public types java.sql exports.
import module java.sql;

// --- a separate file: flexible constructor bodies (JDK 25), validate before super(...).
class PositiveBigInteger extends java.math.BigInteger {
    PositiveBigInteger(long value) {
        if (value <= 0) throw new IllegalArgumentException("non-positive");   // no 'this' used
        super(Long.toString(value));
    }
}

Module imports can create ambiguity: import module java.desktop; and import module java.base; both bring in a List. A single-type import such as import java.util.List; resolves it, because single-type imports shadow on-demand ones. For the module system itself, see Java modules.

What is still in flight

As of JDK 27, which reached general availability on 15 September 2026, primitive types in patterns, instanceof and switch are in their fifth preview (JEP 532, after JEPs 455, 488, 507 and 530). The feature lets a pattern test whether a primitive value can be converted to another primitive type without loss, for example whether an int fits in a byte, and lets switch select on long, float, double and boolean. Five previews tell you the edge cases are still being argued about; do not ship it in production code.

Derived record creation (JEP 468, "withers"), for copying a record with some components changed, is still a Candidate and not in JDK 27; until it lands, write explicit withCents(long) methods.

Watch the Amber project page rather than conference roadmaps. Related work on value types and flattened data lives in a separate project; see Project Valhalla, and for concurrency features see Project Loom.

An adoption plan by LTS release

You runUse freelyHold back
JDK 17var, switch expressions, text blocks, records, instanceof patterns, sealed classesPattern switch is a first preview here; record patterns do not exist yet
JDK 21Everything above plus record patterns and pattern switch; redesign dispatch around sealed types_ is preview in 21 and final in 22; wait for 25 if you stay on LTS
JDK 25Everything above plus _, module imports, flexible constructor bodies, compact source filesPrimitive patterns (preview)

When moving up, raise the language level, let IDE refactorings convert instanceof-and-cast pairs, then remodel one closed domain with sealed records.

Failure modes and trade-offs

SymptomCauseFix
UnsupportedClassVersionError mentioning preview featuresClass compiled with --enable-preview on another releaseNever publish preview-compiled artifacts; recompile per release
New subtype silently handled wrongA default branch swallowed itRemove default from switches over sealed types
MatchException in productionLibrary added a permitted subtype; caller not recompiledRecompile consumers; version sealed APIs
Records break an ORM or serializerFramework expects mutable beans or a no-arg constructorKeep entities as classes; use records for DTOs and messages
Sealed class will not compile across packagesPermitted subtypes outside the module, or outside the package when unnamedCo-locate subtypes, or move to a named module

The deeper trade-off is closed versus open extension. Sealed types plus switches make adding an operation cheap and adding a case deliberately loud. Classic polymorphism does the opposite. Use sealed data where the set of cases is part of your domain, such as events, commands or AST nodes, and keep interfaces open where third parties are meant to plug in.

What to do next

  1. Check which JDK your production runtime and your build target use; that decides which row of the adoption table applies.
  2. Search your codebase for instanceof followed by a cast and for throw new IllegalStateException in dispatch methods; those are migration candidates.
  3. Pick one closed domain, such as events or commands, and convert it to a sealed interface with record implementations.
  4. Replace its dispatch sites with switch expressions without default, and confirm that adding a dummy case breaks the build where you expect.
  5. Ban --enable-preview in library builds with a build check, and allow it only in experiments.
  6. Read the records and pattern matching deep dives before modelling anything with identity or mutable state.
Key takeaway: Project Amber delivers Java's language features one JEP at a time, usually through preview rounds that tie class files to one release. Its core, final since JDK 21, is data-oriented programming: records and sealed types describe data exactly, and exhaustive pattern switches consume it so that a new case becomes a compile error instead of a production bug. JDK 25 adds unnamed variables, module imports, compact source files and flexible constructor bodies; primitive patterns are still in preview in JDK 27 and derived record creation is only a candidate. Adopt by LTS release, avoid preview features in anything you publish, and delete default branches over sealed types.