Skip to main content

Session Serialization: Implementing Serializable and Performance Optimization

Both Tomcat clustering and Redis session sharing use serialization to transmit or store session data externally. Failing to implement serialization correctly results in NotSerializableException errors. Implementing it carelessly leads to performance degradation or version mismatch issues.


What is Serialization?​

Serialization converts a Java object in memory into a byte stream. Deserialization is the reverse.

[Java Object]  ──serialize──▢  [Byte Array]  ──transmit──▢  [Byte Array]  ──deserialize──▢  [Java Object]
UserSession Network/Redis UserSession

Serialization Requirements for Session Objects​

// All objects stored in the session must implement Serializable
HttpSession session = request.getSession();
session.setAttribute("user", userObject); // userObject must be Serializable

Implementing Serializable Correctly​

Basic Implementation​

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;

public class UserSession implements Serializable {

// Always declare serialVersionUID explicitly.
// Without it, the JVM auto-calculates the value β€” which changes when the
// class is modified, causing InvalidClassException on older serialized data.
private static final long serialVersionUID = 1L;

private Long userId;
private String username;
private String email;
private String role;
private LocalDateTime loginTime;
private List<String> permissions;

// transient: excluded from serialization (sensitive data or non-serializable objects)
private transient String rawPassword; // Security: exclude
private transient HttpSession httpSession; // Cannot be serialized

// Default constructor required for deserialization
public UserSession() {}

public UserSession(Long userId, String username, String role) {
this.userId = userId;
this.username = username;
this.role = role;
this.loginTime = LocalDateTime.now();
}

// getters/setters...
}

Nested Objects Also Need Serializable​

// Permission must also implement Serializable
public class Permission implements Serializable {
private static final long serialVersionUID = 1L;

private String resource;
private String action;
}

public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;

private List<Permission> permissions; // Permission must be Serializable
}

Managing serialVersionUID​

Poorly managed serialVersionUID causes deserialization errors during rolling deployments when old and new versions run simultaneously.

When Version Conflicts Occur​

// v1.0: only userId, username
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private Long userId;
private String username;
}

// v1.1: add email field β€” keep same serialVersionUID
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L; // same value
private Long userId;
private String username;
private String email; // New field β€” deserialized as null (intended behavior)
}

With the same serialVersionUID, new fields are initialized to null when deserializing old data. This is expected.

// Dangerous: changing field type or removing a field
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private String userId; // Long β†’ String: deserialization fails!
}

Rules:

  • Adding fields: keep the same serialVersionUID
  • Deleting or changing the type of fields: increment serialVersionUID

Java Serialization Limitations and Alternatives​

Java's built-in serialization is disadvantaged in terms of performance and size.

UserSession (userId, username, role):
Java serialization: ~350 bytes, ~0.5ms
JSON (Jackson): ~80 bytes, ~0.2ms
Kryo: ~50 bytes, ~0.05ms

Spring Session + JSON (GenericJackson2JsonRedisSerializer)​

Spring Session uses JSON serialization by default β€” faster and more readable than Java serialization.

{
"@class": "com.example.model.UserSession",
"userId": 1001,
"username": "admin",
"role": "ADMIN",
"loginTime": ["java.time.LocalDateTime", [2024, 1, 15, 10, 30, 0, 0]]
}

Note: Class type information (@class) is stored with JSON. If the class package or name changes, deserialization will fail. Take care when refactoring.

Minimize Data Stored in Sessions (Most Important)​

// Bad: storing complex objects in session
session.setAttribute("user", userEntityWithAllRelations); // thousands of bytes
session.setAttribute("orders", allOrderHistory); // tens of thousands of bytes

// Good: store only IDs, fetch from DB when needed
session.setAttribute("userId", user.getId()); // 8 bytes
session.setAttribute("role", user.getRole()); // ~10 bytes
// Fetch the rest from DB/cache when needed

Smaller sessions mean less serialization/deserialization time and less Redis memory usage.


Serialization Performance Test​

public class SerializationBenchmark {

public static void main(String[] args) throws Exception {
UserSession session = new UserSession(1001L, "admin", "ADMIN");

// Measure serialization time
long start = System.nanoTime();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(session);
byte[] serialized = baos.toByteArray();
long serializeTime = System.nanoTime() - start;

System.out.printf("Serialized size: %d bytes%n", serialized.length);
System.out.printf("Serialization time: %.3f ms%n", serializeTime / 1_000_000.0);

// Measure deserialization time
start = System.nanoTime();
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
ObjectInputStream ois = new ObjectInputStream(bais);
UserSession deserialized = (UserSession) ois.readObject();
long deserializeTime = System.nanoTime() - start;

System.out.printf("Deserialization time: %.3f ms%n", deserializeTime / 1_000_000.0);
}
}

Session Serialization Checklist​

Pre-deployment verification:

βœ… All classes stored in session implement Serializable
βœ… All Serializable classes have an explicit serialVersionUID declaration
βœ… Non-serializable or sensitive fields have the transient keyword
βœ… Nested objects (including List and Map values) are all Serializable
βœ… Session data is minimized (store IDs only, not full objects)
βœ… serialVersionUID updated when fields are deleted or types changed
βœ… Old-version deserialization tested during rolling deployments

Fixing NotSerializableException​

java.io.NotSerializableException: com.example.model.OrderDetail

When this occurs:

  1. Find the non-serializable class name in the stack trace (OrderDetail)
  2. Add implements Serializable to that class
  3. Add private static final long serialVersionUID = 1L;
  4. Mark any non-serializable fields (Connection, InputStream, etc.) as transient
  5. Redeploy and test
// Before fix
public class OrderDetail {
private Long orderId;
private Connection dbConnection; // cannot be serialized
}

// After fix
public class OrderDetail implements Serializable {
private static final long serialVersionUID = 1L;
private Long orderId;
private transient Connection dbConnection; // add transient
}