Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Interview Prep: 5–8 Years

Advanced Core Java

  • Advanced OOP & Design Scenarios — Interview Questions
  • Advanced Concurrency, Collections & Memory — Interview Questions
  • Modern Java Language Features & Annotations — Interview Questions
  • Class Loading, Reflection, Serialization & Idioms — Interview Questions

Java Design Patterns in Depth

  • Design Patterns Overview & Singleton — Interview Questions
  • Factory & Abstract Factory Patterns — Interview Questions
  • Builder & Prototype Patterns — Interview Questions
  • Adapter & Bridge Patterns — Interview Questions
  • Composite & Decorator Patterns — Interview Questions
  • Facade & Proxy Patterns — Interview Questions
  • Chain of Responsibility & Observer Patterns — Interview Questions
  • Strategy & Template Method Patterns — Interview Questions
  • Command Pattern — Interview Questions

Advanced Spring Boot

  • Logging, Configuration & Actuator (Advanced) — Interview Questions
  • Transactions, Multiple Datasources & Query Tuning — Interview Questions
  • Validation & REST API Design (Advanced) — Interview Questions
  • Reactive, Async & Scheduling in Spring Boot — Interview Questions
  • Deployment, High Availability, Scaling & Caching — Interview Questions
Chaturmind
← Java Interview Prep: 5–8 Years

Advanced Core Java

  • Advanced OOP & Design Scenarios — Interview Questions
  • Advanced Concurrency, Collections & Memory — Interview Questions
  • Modern Java Language Features & Annotations — Interview Questions
  • Class Loading, Reflection, Serialization & Idioms — Interview Questions

Java Design Patterns in Depth

  • Design Patterns Overview & Singleton — Interview Questions
  • Factory & Abstract Factory Patterns — Interview Questions
  • Builder & Prototype Patterns — Interview Questions
  • Adapter & Bridge Patterns — Interview Questions
  • Composite & Decorator Patterns — Interview Questions
  • Facade & Proxy Patterns — Interview Questions
  • Chain of Responsibility & Observer Patterns — Interview Questions
  • Strategy & Template Method Patterns — Interview Questions
  • Command Pattern — Interview Questions

Advanced Spring Boot

  • Logging, Configuration & Actuator (Advanced) — Interview Questions
  • Transactions, Multiple Datasources & Query Tuning — Interview Questions
  • Validation & REST API Design (Advanced) — Interview Questions
  • Reactive, Async & Scheduling in Spring Boot — Interview Questions
  • Deployment, High Availability, Scaling & Caching — Interview Questions
HomeLearnJava Interview PrepJava Interview Prep: 5–8 YearsAdvanced Core Java
✓ FreeAdvanced· 11 min read

Advanced OOP & Design Scenarios — Interview Questions

Immutability for security and stability, builders for complex configuration, refactoring a god class with IS-A/HAS-A and interface segregation, breaking and protecting singletons, deep vs shallow copies, identity-based equals, polymorphism done right, final classes, custom exceptions, and package-by-feature structure.

Published September 25, 2026


How to use this lesson

At 5–8 years, interviewers want design reasoning, not definitions. For each scenario, state the approach, the trade-off you're accepting, and how you'd enforce it (tests, types, reviews). The fundamentals behind these answers are covered in the 2–5 years and fresher tiers.

Q1. Scenario: certain data must stay constant and tamper-proof for its whole lifecycle. How do you design for that?

Short answer: Model it as immutable value objects:

  • final classes with private final fields, and no setters (or a record).
  • Defensive copies of mutable inputs and outputs (List.copyOf, and copying arrays and Dates).
  • Validation in the constructor, so an invalid state can never exist.

Expose read-only views, and publish the objects safely between threads. For values that come from configuration, bind them once at startup into immutable @ConfigurationProperties records.

public record PricingPolicy(BigDecimal taxRate, Set<String> taxExemptSkus) {
    public PricingPolicy {
        if (taxRate.signum() < 0) throw new IllegalArgumentException("taxRate < 0");
        taxExemptSkus = Set.copyOf(taxExemptSkus);          // no external mutation, ever
    }
}

Key points to cover:

  • Immutability limits accidental and malicious modification, gives thread safety for free, and makes objects safe cache keys.
  • For secrets (passwords, keys), immutability isn't enough. Keep them in char[] or byte arrays you can wipe, keep them out of logs and toString, and hold them in a vault or KMS.

Learn it in depth → Records

Q2. Which pattern would you use for an API that creates complex configuration objects?

Short answer: The Builder pattern, typically a static nested builder on an immutable configuration class. Callers set only what they need, with named, fluent methods. Defaults are applied in one place, and build() validates cross-field rules before returning an immutable object.

RetryPolicy policy = RetryPolicy.builder()
        .maxAttempts(5)
        .backoff(Duration.ofMillis(200), 2.0)
        .retryOn(IOException.class, TimeoutException.class)
        .build();                                     // validates: maxAttempts ≥ 1, multiplier ≥ 1, …

Key points to cover:

  • Alternatives: static factories for a few common presets (RetryPolicy.none(), RetryPolicy.standard()), and records with a compact constructor for simple cases. Builders shine with many optional parameters, or staged construction.

Learn it in depth → Builder Pattern

Q3. Refactor a Vehicle class that has both fly() and sail(). How do IS-A and HAS-A relationships help, and how does this relate to the single responsibility principle?

Short answer: A Vehicle with fly() and sail() forces cars to carry methods they can't honour, violating SRP, LSP and ISP. Refactor to:

  • Capabilities as small interfaces (Flyable, Sailable, Drivable), which classes implement: an IS-A relationship with a capability.
  • Shared mechanics through composition: a vehicle HAS-A Engine or Propulsion strategy, instead of inheriting it.

A seaplane can then be both Flyable and Sailable, without deep hierarchies.

interface Flyable { void fly(); }
interface Sailable { void sail(); }

abstract class Vehicle {                          // only what ALL vehicles share
    protected final Propulsion propulsion;        // HAS-A: pluggable
    Vehicle(Propulsion p) { this.propulsion = p; }
}
final class Seaplane extends Vehicle implements Flyable, Sailable {
    Seaplane() { super(new TurbopropPropulsion()); }
    public void fly()  { propulsion.thrust(); /* take-off logic */ }
    public void sail() { propulsion.thrust(); /* water taxi logic */ }
}

Learn it in depth → Liskov Substitution & Interface Segregation

Q4. Why use a builder instead of constructors?

Short answer:

  • Readability: named steps instead of positional arguments. new Pizza(12, true, false, true) is unreadable.
  • Optional parameters without telescoping constructors.
  • Immutability with validation at the end.
  • Consistency: no half-constructed object is ever visible.
  • Evolvability: new optional fields don't break callers.

The cost is extra code, and a heavier API for small objects.

Key points to cover:

  • With Lombok's @Builder, or record-based factories, the boilerplate cost is low. Still, don't use builders for 2–3 required fields. A constructor or record is clearer.

Q5. How can a singleton be broken, and how do you guarantee a single instance?

Short answer: A classic singleton can be broken by:

  • Reflection: calling the private constructor with setAccessible(true).
  • Serialization: deserializing creates a new instance.
  • Cloning.
  • Multiple class loaders: one instance per loader.
  • An unsafe lazy initialisation race.

The defences:

  • An enum singleton, which is immune to reflection and serialization by JVM design.
  • Or: a guard in the constructor that throws if an instance exists, plus readResolve() returning INSTANCE, no Cloneable, and a holder idiom or volatile double-checked locking for laziness.
public enum ConfigRegistry {
    INSTANCE;
    private final Map<String, String> values = new ConcurrentHashMap<>();
    public String get(String key) { return values.get(key); }
}

Key points to cover:

  • In Spring applications, prefer a singleton-scoped bean, injected where it's needed. It's testable, and has no global static state.

Learn it in depth → Singleton Pattern

Q6. What are deep and shallow cloning, and how is Cloneable used?

Short answer: A shallow copy duplicates the top-level object, but shares the referenced objects. Mutating a nested list through the copy affects the original. A deep copy recursively duplicates the nested mutable state too. Cloneable is a marker interface that makes Object.clone() perform a field-by-field (shallow) copy instead of throwing CloneNotSupportedException. You override clone(), make it public, and deep-copy the mutable fields yourself.

Key points to cover:

  • clone() is widely considered broken by design: it bypasses constructors, clashes with final fields, and requires casting. Prefer copy constructors or static factories (Order.copyOf(order)), or immutable objects, which never need copying.
  • Deep-copying by serialization round-trips works, but it's slow and fragile.

Q7. How do you make equals() compare user profiles by their unique identifier?

Short answer: Base both equals and hashCode on the stable identifier. Handle this == o, null and the type check, and keep the behaviour consistent with how profiles are used in collections.

@Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof UserProfile other)) return false;
    return userId != null && userId.equals(other.userId);     // a transient (not yet saved) profile equals only itself
}
@Override public int hashCode() { return getClass().hashCode(); }   // stable across persist, when the ID is generated late

Key points to cover:

  • For JPA entities with generated IDs, an ID-based hashCode changes when the entity is persisted, which breaks HashSets. Hence the constant hashCode above, or a natural business key (such as a UUID assigned at creation).
  • For value objects, base equality on all the value fields (records do this).

Q8. How would you use polymorphism to model different animal behaviours?

Short answer: Define the behaviour in a supertype (an interface or abstract class), and let each subtype override it. Client code depends only on the supertype, and dynamic dispatch picks the right behaviour at runtime. New animals are added without changing existing code (open/closed).

sealed interface Animal permits Dog, Cat, Parrot { String speak(); }
record Dog() implements Animal { public String speak() { return "Woof"; } }
record Cat() implements Animal { public String speak() { return "Meow"; } }
record Parrot(String phrase) implements Animal { public String speak() { return phrase; } }

List<Animal> zoo = List.of(new Dog(), new Cat(), new Parrot("Hello"));
zoo.forEach(a -> System.out.println(a.speak()));

Key points to cover:

  • If behaviours vary along an independent axis (how it moves, how it eats), use composition and strategies, rather than subclassing every combination.

Q9. Design a class that can't be extended, and whose core methods can't be overridden.

Short answer: Declare the class final. Its methods then can't be overridden at all, because no subclass can exist. If the class must stay extensible but some core methods must not change, keep the class non-final, and mark those methods final (the template-method style). Alternatives:

  • A private constructor with static factories.
  • Sealed classes (permits), which allow only known subclasses.

Key points to cover:

  • Frameworks that proxy by subclassing (Spring CGLIB, Hibernate) can't proxy final classes or methods. Keep beans that need AOP, and JPA entities, non-final.

Q10. How do you override equals() for custom equality conditions?

Short answer: Follow the contract (reflexive, symmetric, transitive, consistent, x.equals(null) == false):

  1. Check identity.
  2. Check the type (instanceof pattern, or getClass()).
  3. Cast.
  4. Compare the significant fields with Objects.equals (null-safe), or compareTo == 0 where the scale matters (BigDecimal).

Always override hashCode over the same fields.

Key points to cover:

  • instanceof vs getClass(): instanceof allows subclasses to be equal to the parent, which can break symmetry if a subclass adds fields. getClass() is strict. Or make the class final.
  • For case-insensitive or normalised equality (for example emails), normalise once, in the constructor, rather than in equals.

Q11. It's critical to have only one configuration-manager instance. How would you implement it?

Short answer: In plain Java, use an enum singleton, or the initialization-on-demand holder idiom. Both are thread-safe without explicit locking. In a Spring application, make it a singleton-scoped bean: one per context, injected, testable, and configured from @ConfigurationProperties.

public final class ConfigManager {
    private ConfigManager() { load(); }
    private static final class Holder { static final ConfigManager INSTANCE = new ConfigManager(); }
    public static ConfigManager get() { return Holder.INSTANCE; }      // lazy, thread-safe via class init
}

Key points to cover:

  • "Only one instance" is per JVM and class loader. Across a cluster, you need a different tool: a config server, or leader election for a coordinator.

Q12. Implement a singleton configuration manager, with thread safety. (Same scenario, focusing on the threading)

Short answer: The lazy if (instance == null) version is not thread-safe. The safe options:

  • Eager initialisation.
  • The holder idiom (Bill Pugh), which relies on the JVM's thread-safe class initialisation.
  • Double-checked locking with a volatile field.
  • An enum.

Also make the singleton's state safe: immutable configuration snapshots, swapped atomically on reload through an AtomicReference.

private final AtomicReference<Config> current = new AtomicReference<>(Config.load());
public Config config() { return current.get(); }
public void reload() { current.set(Config.load()); }             // readers never see half-built config

Q13. Describe a scenario where custom exceptions beat built-in ones.

Short answer: When callers must react differently to a specific business failure, or when you need structured context. For example, in payments: InsufficientFundsException(accountId, requested, available) lets the API return a 422 with a clear error code, lets the UI prompt for a top-up, and lets monitoring count business declines separately from system errors. A generic IllegalStateException carries none of that.

public final class InsufficientFundsException extends RuntimeException {
    private final String accountId; private final BigDecimal requested, available;
    public InsufficientFundsException(String accountId, BigDecimal requested, BigDecimal available) {
        super("Account %s: requested %s, available %s".formatted(accountId, requested, available));
        this.accountId = accountId; this.requested = requested; this.available = available;
    }
    public String errorCode() { return "INSUFFICIENT_FUNDS"; }
}

Key points to cover:

  • Keep a small hierarchy, with a base domain exception per module, and map it centrally (@RestControllerAdvice) to stable error codes.

Q14. How would you structure packages for maintainability in a complex project?

Short answer: Package by feature (or bounded context) first, and by layer only within a feature: com.acme.orders.{api, domain, persistence}, com.acme.payments.{…}. Make most classes package-private, and expose each feature through a small public API. Enforce the boundaries with ArchUnit tests or Spring Modulith, so modules don't reach into each other's internals.

com.acme.shop
 ├── orders/        (OrderController, OrderService, Order, OrderRepository — mostly package-private)
 ├── payments/
 ├── catalog/
 └── shared/        (small, stable cross-cutting types only)

Common trap: top-level controller/, service/, repository/ packages for the whole application. Every feature change touches every package, everything has to be public, and boundaries erode. That's the opposite of "package by functionality".

Follow-up questions this topic invites — and their answers

Q: Records or Lombok? A: Records for immutable data carriers, since they're in the language, with no annotation processing. Lombok where you need mutable JPA entities, or builders on non-record classes. Many teams use both, judiciously.

Q: How do you enforce architecture rules automatically? A: ArchUnit tests ("classes in ..domain.. must not depend on ..api..", "no cycles between features"), or Spring Modulith's ApplicationModules.verify() in the test suite.

Q: What's the "fragile base class" problem? A: Subclasses depend on the parent's implementation details, so changing the parent (even internally) can break subclasses. It's a strong reason to prefer composition, and to design classes for inheritance explicitly, or make them final.

Q: When is inheritance still the right choice? A: For a true IS-A relationship where substitutability holds, when the base class is designed for extension (template methods, documented hooks), or when a framework requires it.

Next

Advanced Concurrency, Collections & Memory — Interview Questions

AI Tutor

Lesson: Advanced OOP & Design Scenarios — Interview Questions

Quick actions

AI responses can be inaccurate. Verify critical information.