Designed to accelerate technical prep from core foundations to production-grade, JVM-savvy backend engineering mastery.
Let's revisit the four pillars of OOP. Instead of academic hand-waving, let's look at the actual engineering patterns and real code behind them.
The Idea: Setting up a protective boundary around an object's internal state. You restrict raw access to variables and force external actors to interact exclusively via public methods. This controls state invariants and safeguards consistency.
A class holding sensitive security tokens (such as SonarQube or Apiiro webhook secret keys). The raw token is private to prevent other runtime components from altering or reading it directly. Access is mediated by a getter that decrypts or logs usage dynamically.
public class SecureConfigManager {
private String decryptedToken;
private final String encryptedToken;
public SecureConfigManager(String encryptedToken) {
this.encryptedToken = encryptedToken;
}
// Access is strictly controlled, audited, and lazy-loaded
public synchronized String getDecryptedToken(User caller) {
if (!caller.hasRole("ADMIN")) {
throw new SecurityException("Unauthorized state access!");
}
if (this.decryptedToken == null) {
this.decryptedToken = decrypt(this.encryptedToken);
}
return this.decryptedToken;
}
private String decrypt(String input) {
return new StringBuilder(input).reverse().toString(); // Dummy decryption logic
}
}
You interact with the public interfaceβthe volumeUp() button. The actual circuitry, resistors, and infrared transmitters (private properties) are safely hidden inside the plastic casing.
You cannot reach into the physical vault and alter your balance (unencapsulated state). You request a withdrawal through a keypad interface (public API) which validates your PIN and balance before processing.
The Idea: Designating an IS-A relationship to inherit common state variables and functionality from a parent class, facilitating code reuse and logical hierarchy.
An AbstractAuthenticationProvider class that establishes core identity verification mechanisms, audit logging, and MFA handshakes. Concrete subclasses like OAuthProvider and SAMLProvider inherit this common structure while implementing their respective protocol-specific logic.
public abstract class AbstractAuthenticationProvider {
protected final String providerId;
protected final AuditLogger auditLog;
protected AbstractAuthenticationProvider(String providerId) {
this.providerId = providerId;
this.auditLog = new AuditLogger(providerId);
}
// Inherited common identity verification utility
public void initializeAuditContext(String userId) {
auditLog.log("Authentication attempt from user: " + userId);
}
public abstract AuthenticationToken authenticate(Credentials creds);
}
public class OAuthProvider extends AbstractAuthenticationProvider {
public OAuthProvider() {
super("OAUTH2_OIDC");
}
@Override
public AuthenticationToken authenticate(Credentials creds) {
initializeAuditContext(creds.getUsername()); // Inherited
String accessToken = exchangeCodeForToken(creds.getOAuthCode());
return new AuthenticationToken(creds.getUsername(), accessToken);
}
private String exchangeCodeForToken(String code) {
// OAuth2 token exchange logic
return "eyJhbGciOiJIUzI1NiIs...";
}
}
A Dog inherits generic metabolic and sleeping mechanisms from the Mammal superclass, but implements its own distinct behavior, bark().
An ElectricCar extends the core template of Car (inheriting steering, braking, and chassis), but replaces a gas-based fuel mechanism with high-density battery charging.
The Idea: "Many forms." Allowing different underlying implementations to respond to the identical method invocation in their own unique way. Dynamic (runtime) polymorphism is achieved via method overriding.
An interface named MFAChannel defines sendChallenge(Identity user). Dynamic implementations exist for EmailMFAChannel and SMSMFAChannel. Your backend loops over a generic list of channels and sends verification codes without caring about the underlying transport layers.
public interface MFAChannel {
void sendChallenge(String recipientId, String verificationCode);
}
public class EmailMFAChannel implements MFAChannel {
@Override
public void sendChallenge(String recipientId, String code) {
System.out.println("Sending email verification code to: " + recipientId);
}
}
public class SMSMFAChannel implements MFAChannel {
@Override
public void sendChallenge(String recipientId, String code) {
System.out.println("Sending SMS verification code to: " + recipientId);
}
}
You command your pet to "Speak!". A dog responds by barking, whereas a cat responds by meowing. The execution depends entirely on the dynamic runtime type of the receiver.
One port interface interacts with chargers, audio devices, or ultra-fast storage drives. The computer transmits data via a standard protocol, while the connected hardware handles its distinct conversion task.
The Idea: Hiding operational complexity and only showing the structural contract of "what" an object does rather than "how" it does it.
An IdentityTokenManager interface exposes a single method: issueToken(User user). Under the hood, the implementing classes coordinate JWT claims generation, signature validation, token encryption, and revocation list checking.
public interface IdentityTokenManager {
String issueToken(User user);
}
public class JwtIdentityTokenManager implements IdentityTokenManager {
@Override
public String issueToken(User user) {
// The complex JWT generation process is abstracted away from caller
Claims claims = buildSecurityClaims(user);
String jwtToken = encodeAndSignToken(claims);
verifyTokenRevocation(user);
return jwtToken;
}
private Claims buildSecurityClaims(User u) { /* JWT claim construction */ }
private String encodeAndSignToken(Claims c) { /* HMAC signature generation */ }
private void verifyTokenRevocation(User u) { /* Revocation list check */ }
}
To slow down, you step on the brake pedal. You don't manage master cylinders, hydraulic fluid pressures, or brake pads pinching rotors.
You press the "Double Shot" button (Abstract API). You do not manually adjust the boiler temperature, pump pressure, or water pre-infusion timings.
Modifiers control visibility and compile-time/runtime behaviors. Let's make sure you never mess up visibility in a code review again.
| Modifier | Same Class | Same Package | Subclass (Different Package) | World (Anywhere) |
|---|---|---|---|---|
private |
β Yes | β No | β No | β No |
default (No Keyword) |
β Yes | β Yes | β No | β No |
protected |
β Yes | β Yes | β Yes | β No |
public |
β Yes | β Yes | β Yes | β Yes |
public or package-private/default. They cannot be declared as private or protected because a standalone class needs package-level context to exist cleanly.These modifiers alter the structural and execution behaviors of class definitions, methods, and variables.
static Class-Level LifecycleBelongs directly to the Class template itself, rather than to instances. Memory is allocated once in the JVM **Metaspace** (formerly PermGen) during class loading.
public class SecurityLogger {
// Shared by all instances of the class - thread safe if read-only
private static final Logger logger = LoggerFactory.getLogger(SecurityLogger.class);
// Helper static method. No class instance instantiation is needed to call it.
public static boolean isValidToken(String token) {
return token != null && token.length() > 32;
}
}
final ImmutabilityPrevents structural changes. Variables cannot be reassigned; methods cannot be overridden; classes cannot be extended.
public final class ImmutableScannerConfig { // Cannot be extended
private final String apiKey; // Must be initialized exactly once
public ImmutableScannerConfig(String apiKey) {
this.apiKey = apiKey;
}
public final String getApiKey() { // Cannot be overridden in subclasses
return apiKey;
}
}
transient Serialization ExclusionTells the JVM not to serialize this field when writing the object state into an output byte-stream.
import java.io.Serializable;
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
// Will be completely ignored by Java Serialization and initialized as null upon deserialization
private transient String rawSessionPassword;
}
volatile Multi-Threaded VisibilityForces all read and write operations to go directly to **Main CPU Memory**, completely bypassing thread local CPU caches (L1, L2). This guarantees that writes by one thread are immediately visible to all other reader threads.
public class ServiceStatusMonitor implements Runnable {
// Without volatile, thread-local caches may loop indefinitely using stale memory values
private volatile boolean active = true;
public void shutdown() {
active = false;
}
@Override
public void run() {
while (active) {
// Execute application loop...
}
}
}
Nested classes help group classes that are only used in one place, keeping your code cleaner and more organized. Let's look at exactly when and how to deploy them in high-throughput backend services.
A static class declared inside an outer class. It behaves like any normal package-level class, but is physically nested for clean grouping. It does *not* hold an implicit reference to an instance of the outer class, making it memory-efficient. Use it whenever the nested class does not need access to the outer class's instance fields (e.g., the Builder Pattern).
public class ApiConfig {
private final String endpoint;
private ApiConfig(Builder builder) {
this.endpoint = builder.endpoint;
}
// Static Nested Class (Often used for the Builder Pattern)
public static class Builder {
private String endpoint;
public Builder setEndpoint(String endpoint) {
this.endpoint = endpoint;
return this;
}
public ApiConfig build() {
return new ApiConfig(this);
}
}
}
Tied directly to an instance of the outer class. It can access all instance fields of the outer class directly, including private ones. However, this hidden back-reference can easily lead to unexpected memory leaks if the inner class instances outlive the outer parent. Use them sparingly (e.g., for custom Iterators or localized State wrappers within a manager).
public class TransactionManager {
private final String masterId = "DB_CORE_PROD";
public class TransactionSession { // Non-static Inner Class
public void printMetadata() {
// Directly accesses the outer class's private field!
System.out.println("Executing query against resource: " + masterId);
}
}
}
A Senior Developer must understand the underlying performance characteristics, memory layouts, and structural thread-safety of collection classes.
| Class Name | Ordering | Duplicates Allowed? | Thread Safe? | Primary Complexity (Get / Put) |
|---|---|---|---|---|
ArrayList |
Insertion Order | β Yes | β No | O(1) read, O(N) array resize write |
LinkedList |
Insertion Order | β Yes | β No | O(N) traversal lookup, O(1) insert at node |
HashSet |
None | β No | β No | O(1) average lookup/insert |
TreeSet |
Natural / Comparator Sorted | β No | β No | O(log N) balance tree parsing |
HashMap |
None | β No (Keys Only) | β No | O(1) average hash mapping |
ConcurrentHashMap |
None | β No (Keys Only) | β Yes | O(1) concurrent bucket locking / CAS |
CopyOnWriteArrayList |
Insertion Order | β Yes | β Yes | O(1) read, O(N) copy array write |
ArrayList: "I'm backed by a dynamic array! If you want to fetch an element by index, I can jump straight to the index instantly (O(1)). But if you insert elements into the middle, I have to copy and shift the rest of the array down, which is painful (O(N))."
LinkedList: "I don't use arrays. I link elements using pointers! To insert, I just link up the new node (O(1)). But if you want to find an element, I have to traverse my nodes one by one (O(N))."
The Verdict: Always default to ArrayList in modern microservices. Cache locality and sequential memory layouts make array iteration orders of magnitude faster on physical hardware than pointer-hopping.
If you use a custom object as a key in a HashMap, you *must* override equals and hashCode to prevent duplicate entries and memory leaks.
import java.util.Objects;
public class ClientKey {
private final String clientId;
private final String secretVersion;
public ClientKey(String clientId, String secretVersion) {
this.clientId = clientId;
this.secretVersion = secretVersion;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClientKey that = (ClientKey) o;
return Objects.equals(clientId, that.clientId) &&
Objects.equals(secretVersion, that.secretVersion);
}
@Override
public int hashCode() {
return Objects.hash(clientId, secretVersion);
}
}
Instead of search operations, use LinkedList through the Deque interface for fast FIFO or LIFO queue processing inside local rate-limiters.
import java.util.Deque;
import java.util.LinkedList;
public class RateLimiterQueue {
private final Deque<Long> timestampLogs = new LinkedList<>();
public synchronized boolean isAllowed(long maxRequests, long timeWindowMs) {
long now = System.currentTimeMillis();
while (!timestampLogs.isEmpty() && (now - timestampLogs.peekFirst() > timeWindowMs)) {
timestampLogs.pollFirst(); // O(1) removal from the head
}
if (timestampLogs.size() < maxRequests) {
timestampLogs.addLast(now); // O(1) append to tail
return true;
}
return false;
}
}
Unlike old legacy wrappers like Collections.synchronizedMap() which lock the entire map, ConcurrentHashMap achieves thread-safety using dynamic optimization:
volatile node properties to read without acquiring locks.import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public class ThreadSafeCache {
// Bucket-level locked structure for dynamic updates
private final ConcurrentHashMap<String, String> activeSessions = new ConcurrentHashMap<>();
// Snapshot array structure. Reads are extremely fast, but updates copy the array.
private final CopyOnWriteArrayList<String> systemHooks = new CopyOnWriteArrayList<>();
public void registerSession(String id, String meta) {
activeSessions.putIfAbsent(id, meta); // Atomic execution
}
}
loadFactor * capacity (default threshold is 75%), the map doubles its bucket size. It creates a new bucket array and rehashes the existing entries. Since Java 8, if a bucket collision count exceeds 8 and total capacity is at least 64, the linked list structure converts into a balanced Red-Black Tree (O(log N) lookup).Generics enforce compile-time type-safety. You must know how to design extensible domain interfaces with wildcards and the **PECS Rule**.
? extends T): If your structure yields objects to be read, declare it with extends. You can read, but cannot write.? super T): If your structure consumes objects to store them, declare it with super. You can write, but cannot safely read.import java.util.List;
// 1. Generic API Wrapper Pattern
public class ApiResponse<T> {
private final T payload;
private final int code;
public ApiResponse(T payload, int code) {
this.payload = payload;
this.code = code;
}
public T getPayload() { return payload; }
}
// 2. Real World Domain Wildcard Pipeline
abstract class LogEntry {}
class SecurityLog extends LogEntry {}
class TelemetryProcessor {
// PRODUCER - Reads items from the list. Must extend parent class LogEntry.
public void analyzeTelemetry(List<? extends LogEntry> telemetryStream) {
for (LogEntry log : telemetryStream) {
System.out.println("Analyzing class: " + log.getClass().getSimpleName());
}
}
// CONSUMER - Inserts items into target collection. Must accept superclasses of SecurityLog.
public void remediateThreats(List<? super SecurityLog> targetCollection) {
targetCollection.add(new SecurityLog()); // Perfectly safe compile-time write execution
}
}
Process: A heavy, isolated execution environment provided by the operating system. It has its own private address space and heap memory. Inter-process communication (IPC) is highly complex and costly.
Thread: A lightweight path of execution spawned inside a process. All threads of a process share the same Heap memory, but each thread manages its own private **Thread Stack** (storing local variables, method invocations) and **Program Counter (PC)**.
An OS/JVM component that assigns executing CPU time to threads in the RUNNABLE state. Java scheduling is typically **preemptive and priority-based**. If a higher-priority thread requests resources, the scheduler suspends lower-priority threads. Because scheduling relies on native OS threads, it is inherently non-deterministic.
join() MethodTells the current executing thread to block and yield control until the target thread completes its execution.
public class PipelineRunner {
public static void main(String[] args) throws InterruptedException {
Thread identityVerificationTask = new Thread(() -> {
System.out.println("Verifying user identity via OAuth provider...");
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.out.println("Identity verification completed securely.");
});
identityVerificationTask.start();
// Tells main thread: "Wait right here until identityVerificationTask finished execution"
identityVerificationTask.join();
System.out.println("Proceeding to session token generation phase...");
}
}
Creating platform threads is expensive (requiring kernel allocation). In enterprise backends, we use thread pools managed by ExecutorService to reuse pre-allocated threads.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class IdentityTokenProcessor {
public void processUserIdentities() {
// Creates a fixed-size pool of reusable system threads
try (ExecutorService pool = Executors.newFixedThreadPool(8)) {
for (int i = 0; i < 100; i++) {
final int userId = i;
pool.submit(() -> {
System.out.println("Processing identity token for user " + userId + " on: " + Thread.currentThread().getName());
});
}
} // Executor auto-closes and shutdowns here
}
}
Let's make sure you master functional programming structures alongside runtime resource protections.
A Functional Interface is an interface with exactly *one* abstract method. Annotating it with @FunctionalInterface is optional but protects it from accidental method additions. Lambdas provide immediate inline implementations of functional contracts without the garbage of anonymous inner classes.
A custom functional interface designed to run custom identity verification checks against user credentials before granting access.
@FunctionalInterface
public interface IdentityValidator {
boolean validate(String username, String password);
}
// Inline usage in an authentication registry
public class AuthenticationRegistry {
public void executeValidation() {
// Expressing behavior immediately as a Lambda
IdentityValidator prodCheck = (user, pass) -> user.startsWith("admin_") && pass.length() > 12;
boolean result = prodCheck.validate("admin_root", "SecurePassword123!");
System.out.println("Authentication validation passing status: " + result);
}
}
Streams provide a declarative, pipeline-based approach to collection processing. Intermediate operations are **lazy** and return another stream; terminal operations are **eager** and close the stream.
import java.util.List;
import java.util.stream.Collectors;
public class VulnerabilityFilter {
public record UserCredential(String id, String role, boolean mfaRequired) {}
public List<String> findUnverifiedAdmins(List<UserCredential> inputList) {
return inputList.stream()
.filter(u -> "ADMIN".equals(u.role())) // Intermediate
.filter(u -> u.mfaRequired()) // Intermediate
.map(UserCredential::id) // Intermediate
.collect(Collectors.toList()); // Terminal execution
}
}
A wrapper class introduced to eliminate standard NullPointerExceptions by forcing developers to handle empty checks explicitly.
import java.util.Optional;
public class IdentityVerificationService {
public Optional<String> validateUserIdentity(String jwtToken) {
return Optional.ofNullable(verifyAndDecodeJwt(jwtToken));
}
private String verifyAndDecodeJwt(String token) {
return null; // Simulated invalid token
}
public void executeTask() {
String userId = validateUserIdentity("eyJhbGciOiJIUzI1NiIs...")
.map(String::trim)
.orElseThrow(() -> new IllegalArgumentException("Invalid JWT token"));
}
}
Simplifies resource cleanup by automatically closing objects that implement java.lang.AutoCloseable at the end of the block, even if an exception is thrown.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ConfigLoader {
public static String readConfig(String targetFile) throws IOException {
// The BufferedReader is automatically closed when the try block exits
try (BufferedReader reader = new BufferedReader(new FileReader(targetFile))) {
return reader.readLine();
}
}
}
How exceptions flow dictates the reliability and diagnostic health of your backend application.
Checked Exceptions: Must be declared in the method signature or handled in a try-catch block at compile-time. They represent recoverable environmental failures (such as standard filesystem or database connection issues).
Unchecked Exceptions (RuntimeExceptions): Represent programming and implementation failures (such as null pointers or arithmetic errors). Declaring them is optional.
In modern microservices, checked exceptions are caught early and translated into domain-specific unchecked exceptions . These are then handled globally to prevent stack trace leaks.
// Custom Unchecked Domain Exception for Authentication
public class InvalidCredentialsException extends RuntimeException {
public InvalidCredentialsException(String msg) {
super(msg);
}
}
// Spring Boot Global Controller Exception Handler for Identity Management
@RestControllerAdvice
public class IdentityExceptionHandler {
@ExceptionHandler(InvalidCredentialsException.class)
public ResponseEntity<ErrorResponse> handleAuthFailure(InvalidCredentialsException ex) {
ErrorResponse response = new ErrorResponse("AUTH_FAILED", ex.getMessage());
return ResponseEntity.status(401).body(response);
}
}
Interfaces define decoupling boundaries. Let's see how interfaces allow us to swap out database engines or payment providers effortlessly.
// Decoupled Core Abstraction for Authentication
public interface AuthProvider {
boolean authenticate(String username, String password);
String getProviderName();
}
// Concrete Implementation 1: OAuth2 Integration
public class OAuth2Provider implements AuthProvider {
@Override
public boolean authenticate(String username, String password) {
// OAuth2 token validation logic goes here
return true;
}
@Override
public String getProviderName() {
return "OAuth2_OIDC";
}
}
// Core service is loosely coupled and easily testable with different auth providers
public class IdentityManagementService {
private final AuthProvider authProvider;
// Dependency Injection points to interface, not implementation
public IdentityManagementService(AuthProvider authProvider) {
this.authProvider = authProvider;
}
public void executeLogin(String user, String pass) {
authProvider.authenticate(user, pass);
}
}
How we package our bytecode determines how it runs in production environments.
| Dimension | JAR (Java ARchive) | WAR (Web ARchive) |
|---|---|---|
| Execution Method | Self-executing via an embedded engine (e.g., java -jar app.jar). |
Requires a standalone servlet container (e.g., Tomcat, WildFly). |
| Directory Layout | Simple flat class structure (/classes, /META-INF). |
Traditional servlet structure (/WEB-INF, /WEB-INF/lib). |
| Embedded Server | β Yes, contains an embedded container (such as Spring Boot's Tomcat or Netty). | β No, relies entirely on the external host's runtime environment. |
Let's look at the key features that have defined Java's evolution in recent years, keeping it competitive with modern languages.
var) Java 10Syntactic sugar that lets the compiler infer the type of local variables, reducing boilerplate while maintaining compile-time safety.
var users = new ArrayList<String>(); // Inferred as ArrayList<String>
Immutable data carriers that auto-generate getters, constructor, equals, hashCode, and toString behind the scenes.
public record UserDto(String id, String email) {}
Gives you strict control over inheritance. By using the sealed keyword, you define exactly which subclasses are allowed to extend or implement your class/interface using the permits clause.
// Only SuccessfulAuth and FailedAuth can implement this interface
public sealed interface AuthenticationResult permits SuccessfulAuth, FailedAuth {}
public final class SuccessfulAuth implements AuthenticationResult {
// User successfully authenticated. Token issued.
private final String accessToken;
public SuccessfulAuth(String token) {
this.accessToken = token;
}
}
public final class FailedAuth implements AuthenticationResult {
// Authentication failed with specific reason
private final String reason;
public FailedAuth(String reason) {
this.reason = reason;
}
}
switch & Switch Expressions Java 21Allows pattern matching in switch statements, making complex type-checking code much cleaner and more readable. Uses switch expressions to directly assign variable values.
public String processAuthenticationAttempt(Object authPayload) {
// Switch Expression yielding a String directly
return switch (authPayload) {
case Integer otp -> "Processing OTP: " + otp;
case String password -> "Authenticating with password";
case UserDto u -> "Federated identity: " + u.email();
case null -> "Missing authentication payload";
default -> "Unsupported auth method: " + authPayload;
};
}
Lightweight threads that run on top of standard platform threads. They are incredibly cheap to create, allowing you to run millions of virtual threads concurrently for high-throughput, I/O-bound applications.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// Perfect for standard blocking network calls
// Under the hood, the underlying carrier thread is automatically released
});
}
Introduces unified interfaces (like SequencedCollection) for collections with a defined encounter order, making it easy to access first and last elements.
SequencedCollection<String> queue = new LinkedHashSet<>();
String first = queue.getFirst();
String last = queue.getLast();
Extends the Streams API to support custom intermediate operations, such as sliding windows.
var windows = numbers.stream()
.gather(Gatherers.windowSliding(3))
.toList(); // e.g., [[1, 2, 3], [2, 3, 4]]
Allows statements (such as parameter validation) to run before calling super() in constructors, making child classes safer and more robust.
class ChildClass extends BaseClass {
ChildClass(String val) {
if (val == null) throw new IllegalArgumentException();
super(val); // Does not need to be the very first line anymore!
}
}
Adds native HTTP/3 support to the standard HttpClient API, enabling faster, more reliable web connections.
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_3)
.build();
These are the high-level concepts that interviewers use to distinguish senior engineers from mid-level developers.
equals() require hashCode()?Answer: This is a fundamental contract in Java. If two objects are equal according to equals(), they must have the same hash code. If they don't, hashing-based collections like HashMap or HashSet won't be able to locate your objects properly, leading to duplicate entries or "lost" keys.
Java is strictly pass-by-value. When you pass an object to a method, Java passes a copy of the memory address (the reference pointer) by value. This means you can modify the object's properties inside the method, but you cannot change the caller's variable to point to a new object entirely.
ArrayList, HashMap): If you try to modify the collection while iterating over it, the iterator throws a ConcurrentModificationException immediately.ConcurrentHashMap, CopyOnWriteArrayList): Iterates over a copy or snapshot of the collection, allowing you to modify the original safely during iteration without throwing exceptions.The Concept: Serialization is the process of converting an object's dynamic state into a flat byte-stream (so it can be saved to a database or sent over a network). Deserialization is the reverse process.
serialVersionUID: A unique version number that verifies that the sender and receiver of a serialized object have loaded classes that are compatible. If you don't declare one, the JVM generates it dynamically, but any minor layout change will throw an InvalidClassException during deserialization.import java.io.Serializable;
public class IdentityToken implements Serializable {
// Explicit identifier ensures runtime compatibility across different authentication servers
private static final long serialVersionUID = 42L;
private String jwt;
private transient String unencryptedClaimData; // Explicitly ignored during serialization for security
public IdentityToken(String jwt, String claimData) {
this.jwt = jwt;
this.unencryptedClaimData = claimData;
}
}