Blog

How To Ensure Serializable Sessions in Vaadin 25 Applications

By  
Tatu Lund
Tatu Lund
·
On May 21, 2026 5:08:49 PM
·

In many real-world Vaadin 25 applications, strict session serialization isn’t always required. However, it becomes important in clustered deployments, failover setups, session passivation scenarios, and environments with frequent restarts.

The good news is that this is manageable when you apply a few disciplined patterns.

This tutorial covers the key patterns for Vaadin 25 (Flow), including Kubernetes Kit tooling, and explains UnserializableComponentWrapper for components that cannot be made serializable directly.

Why This Matters

Vaadin stores server-side UI state in VaadinSession, which is backed by HttpSession. If that session is serialized, every object reachable from the UI graph must also be serializable, or serialization/deserialization will fail.

Common symptoms when this isn’t handled well:

  • Random session loss after restart, rollout, or node switch. Users get logged out or lose their UI state with no obvious error
  • Serialization exceptions caused by hidden object references to non-serializable objects buried deep in the object graph
  • Deserialization failures caused by captured lambdas, anonymous listener classes, or runtime state that doesn't survive the round trip

The underlying reason these bugs are hard to detect is that Java's Serializable interface is a marker, and it makes no guarantees about the objects you reference. A class that implements Serializable can still blow up at runtime if any of its non-transient fields are not serializable. You don't find out until serialization is actually attempted.

The Core Rule

Think in object graphs, not individual classes. A class can implement Serializable, but if it references a non-serializable object that is not marked transient, session replication still fails.

The patterns below are all applications of this one principle.

Pattern 1: Make UI Graph Objects Serializable

Any object that becomes part of the live UI graph; views, presenters, component models, event handlers attached to UI components, needs to implement Serializable. This is the starting baseline.

A minimal example:

```java
public class PurchasesApprovalsPresenter implements Serializable {

// durable state + behavior
}
```

This is necessary but not sufficient. Marking the class Serializable only tells the JVM to attempt serialization; it doesn't guarantee that all reachable state is actually serializable. That's what the remaining patterns address.

Pattern 2: Mark Runtime-Only Dependencies as transient

Service handles, executors, futures, and other runtime-only resources should not be serialized.

Minimal example:

```java
private transient PurchaseService purchaseService;

private transient CompletableFuture<Void> updateTask;
```

Why this helps:

  • Many service objects (especially Spring-proxied beans) are not serializable by design.
  • Even if they happen to be serializable, serializing them would capture stale infrastructure state (database connections, thread pools) that's meaningless after deserialization.
  • It keeps your serialized session lean. Accidentally serializing a service that holds a large cache can silently balloon session sizes and hurt replication performance.

Pattern 3: Rehydrate transient Fields Lazily

After deserialization, transient fields are null. If your code doesn't account for this, you'll get NullPointerExceptions in methods that previously worked fine, because they were never called on a freshly-deserialized instance before. The safest pattern is lazy rehydration through accessor methods:

```java
private PurchaseService getPurchaseService() {

if (purchaseService == null) {
purchaseService = PurchaseService.get();
}
return purchaseService;
}
```

In Spring + Kubernetes Kit setups, transient Spring-bean fields can be reinjected automatically via @SpringComponent and the Kubernetes Kit transient injection mechanism, removing the need for manual lazy initialization in many cases. See the Kubernetes Kit documentation for how to configure this.

Pattern 4: Use Serializable Callback Types

Java's standard java.util.function.* interfaces, Consumer, Supplier, Runnable, Function, do not implement Serializable. If you store instances of these in your component state, you'll get a serialization failure.

Vaadin provides serializable equivalents for the most common functional types:

```java
public interface DecisionConfirmedListener extends Serializable {

void onConfirmed(String comment);
}
```
```java
private SerializableConsumer<Product> onSuccess;
```

Avoid plain java.util.function.* types for callback state stored in the UI graph.

Pattern 5: Clean Up on Detach

Detach is where you break references to active runtime work.

Minimal example:

```java
@Override

protected void onDetach(DetachEvent detachEvent) {
super.onDetach(detachEvent);
if (updateTask != null) {
updateTask.cancel(true);
updateTask = null;
}
}
```

What belongs in onDetach:

  • Cancelling in-flight async tasks (CompletableFuture, ScheduledFuture, etc.)
  • Unregistering backend event listeners
  • Dropping strong references to any external runtime resources that the component was holding

Pattern 6: Prefer Explicit Listener Lifecycle

Event listeners deserve their own pattern because they're easy to get wrong in ways that don't immediately surface as bugs.

An unregistered listener keeps the listener object (and everything it transitively references) alive for as long as the event source lives, which may be the entire application lifetime.

```java
private transient Registration busRegistration;


@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
busRegistration = eventBus.register(this::handleEvent);
}

@Override
protected void onDetach(DetachEvent detachEvent) {
if (busRegistration != null) {
busRegistration.remove();
busRegistration = null;
}
super.onDetach(detachEvent);
}
```

Use explicit register/unregister, weak-listener approaches, or both.

Pattern 7: Enable Kubernetes Kit Session Replication Debug Tool

Vaadin 25 and Kubernetes Kit adds a development-time debug tool that serializes and deserializes the session after every request, then reports any failures to the log. This turns what would otherwise be a silent production failure into a loud, immediate development-time error.

Important: This tool is for development and diagnostics only. Do not enable it in production, it adds significant overhead to every request.

In Spring Boot projects with Kubernetes Kit, the tool is auto-configured. You can also register it explicitly

7.1 Enable in application properties

```properties
vaadin.devmode.sessionSerialization.enabled=true

vaadin.serialization.transients.include-packages=com.example.application
```

Notes:

  • vaadin.devmode.sessionSerialization.enabled turns on the debug tool in dev mode.
  • vaadin.serialization.transients.include-packages narrows transient-field inspection to your app packages.

7.2 Enable extended Java serialization diagnostics

The JVM's built-in extended debug output for serialization gives you much more detail about exactly which class in the graph caused a failure. Enable it via Maven plugin JVM arguments:

```xml
<plugin>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>
--add-opens java.base/java.io=ALL-UNNAMED
-Dsun.io.serialization.extendedDebugInfo=true
</jvmArguments>
</configuration>
</plugin>
```

Optional timeout tuning:

```properties
vaadin.serialization.timeout=60000
```

7.3 Optional explicit registration (as in this demo)

```java
@Bean

VaadinServiceInitListener serializationDebugInitListener() {
return new SerializationDebugRequestHandler.InitListener();
}

@Bean
@Order(Integer.MIN_VALUE)
FilterRegistrationBean<SerializationDebugRequestHandler.Filter> sessionSerializationDebugToolFilter() {
FilterRegistrationBean<SerializationDebugRequestHandler.Filter> registration =
new FilterRegistrationBean<>(new SerializationDebugRequestHandler.Filter());
registration.setAsyncSupported(true);
return registration;
}
```

7.4 Reading results

Typical outcomes in logs are SUCCESS, SERIALIZATION_FAILED, DESERIALIZATION_FAILED, and NOT_SERIALIZABLE_CLASSES.

For SerializedLambda failures, read the BEST CANDIDATES section from bottom to top to find the captured class and method.

Pattern 8: Debug Session Sizes During Serialization

In addition to pass/fail outcomes, Kubernetes Kit can help you measure serialized session size. This is useful when sessions become large and replication slows down.

8.1 Enable debug logging

This demo app enables Kubernetes Kit debug logging in application.properties:

```properties
logging.level.com.vaadin.kubernetes=debug
```

8.2 Read size from serializer log lines

Look for serializer lines that end with elapsed time and payload size in bytes.

Example:

Serializer : Serialization of attributes
[com.vaadin.flow.server.VaadinSession.springServlet,
springServlet.lock,
org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN,
clusterKey, SPRING_SECURITY_CONTEXT]

for session DEBUG-SERIALIZE-F159D9419778EFF9F4DD004B415083A3

with distributed key 4f1af8e8-b432-4cdc-ab98-376c8b188b99_SOURCE:F159D9419778EFF9F4DD004B415083A3

completed in 31ms (124599 bytes)

Key signal:

  • The value in parentheses (for example 124599 bytes) is the serialized session size for that cycle.

8.3 Practical usage

  • Compare size before and after opening heavy views.
  • Track growth over navigation flows; sudden jumps often reveal large objects captured into session state.
  • Use this together with serialization debug outcomes to prioritize fixes.
  • Disable debug-level Kubernetes logging after investigation to reduce log noise.

Pattern 9: Wrap Naturally Non-Serializable Components

Some third-party UI components are not fully serializable by nature. Vaadin Kubernetes Kit provides UnserializableComponentWrapper<S, T> for these cases.

Use this only when making the component itself serializable is not feasible.

Spreadsheet example (Apache POI workbook state)

```java
record SpreadsheetState(byte[] workbookBytes) implements Serializable {

}
Spreadsheet spreadsheet = new Spreadsheet();
var wrapper = new UnserializableComponentWrapper<SpreadsheetState, Spreadsheet>(
spreadsheet,
sheet -> {
try (var out = new ByteArrayOutputStream()) {
sheet.getWorkbook().write(out);
return new SpreadsheetState(out.toByteArray());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
},
state -> {
try (var in = new ByteArrayInputStream(state.workbookBytes())) {
var workbook = new XSSFWorkbook(in);
var restored = new Spreadsheet();
restored.setWorkbook(workbook);
return restored;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
add(wrapper);
```

Important wrapper behavior:

  • Serializer must extract only durable, serializable state.
  • Deserializer rebuilds the component from scratch after graph restoration.
  • You must restore needed behavior (for example, listeners or runtime callbacks).
  • Wrapped component attach/detach listeners are still triggered.

Use Linters as an Early Warning System

Static analysis (for example SonarQube) is useful to detect classes implementing Serializable but holding non-serializable non-transient fields.

What this gives you:

  • Fast feedback in development and code review.
  • Early detection before runtime replication tests.

What it does not give you:

  • It cannot determine developer intent (should this field be serialized, or made transient and rehydrated?).
  • It won't catch failures that only manifest through object graph wiring — a field might look fine in isolation but cause failures when wired together with collaborators.

The Most Important Verification: Serialize the View Graph in Tests

Do not rely only on marker-interface checks or static analysis. The only reliable verification is serializing and deserializing a real, fully-wired view graph in an actual test.

This demo uses SerializationDebugUtil in UI unit tests:

```java
@Test

void viewGraphIsSerializable() throws IOException {
var view = navigate(AddressesView.class);
SerializationDebugUtil.assertSerializable(view);
}
```

Why this is more powerful than static analysis:

  • It validates wiring of view + presenter + collaborators in the same configuration used at runtime..
  • It catches real graph failures, not theoretical ones.
  • It helps pinpoint offending references quickly.

What Level of Coverage Is Enough?

A practical threshold for most applications:

  • Every major view has at least one serialization assertion covering its initial render.
  • Views with significant interaction states (forms, dialogs, multi-step flows) also have assertions covering those intermediate states (not just the initial render).
  • There are no alternate wiring paths (different constructors, feature flags, user-type variations) bypassing tested code.

A Quick Checklist You Can Use Today

  • UI graph collaborators implement Serializable where appropriate.
  • Runtime-only fields are transient.
  • Transient fields are rehydrated safely.
  • Callback and listener state uses serializable interfaces.
  • onDetach cleanup removes runtime work and listeners.
  • Kubernetes Kit debug tool is enabled in development.
  • Kubernetes Kit debug logging is used to track serialized session size when investigating session growth.
  • vaadin.serialization.transients.include-packages is scoped to app packages.
  • Non-serializable third-party components use UnserializableComponentWrapper with explicit serializer/deserializer.
  • View-level tests assert serializability after key interactions.

Final Takeaway

Serializable sessions in Vaadin 25 are not a single trick you apply once. They're the result of a consistent set of habits applied across the entire development lifecycle:

  • Object-graph discipline at design time: think about what belongs in a durable session state and what belongs in transient runtime resources.
  • Explicit lifecycle management for listeners and async work, with clean attach/detach pairs.
  • Continuous verification through the Kubernetes Kit debug tool during development and through serialization assertions in tests.

If you follow these patterns consistently, most production-grade session replication issues are caught early, while still keeping development ergonomics reasonable.

Happy coding!

Tatu Lund
Tatu Lund
Tatu Lund has a long experience as product manager in different industries. Now he is head of the team delivering Vaadin support and training services. You can follow him on Twitter - @ TatuLund
Other posts by Tatu Lund