Study notes

STUDY NOTES · TECHNICAL FUNDAMENTALS

OOP & design principles
Code that bends instead of breaking.

SOLID with real examples, composition vs inheritance, the patterns you'll actually use, dependency injection, interfaces vs abstract classes, immutability, and spotting bad design.

anuragdevon 11th Nov 2026 6 min read Technical fundamentals · Note 1 of 7

Design-principle questions are rarely about definitions. Anyone can recite SOLID. What interviewers want is the moment where a principle saved you: a change that stayed in one file because the code was shaped right. Each section below starts with the 30-second answer, then the details you’ll need for the follow-ups.

// THE SHORT VERSION
  • SOLID keeps change local. SRP and DIP matter most in practice.
  • Prefer composition; inherit only for a true is-a that honours the parent’s contract.
  • Name patterns inside a story, not as a list.
  • Constructor injection makes dependencies explicit and tests easy.
  • Immutable objects are thread-safe for free.
  • Good design: high cohesion, low coupling.
01

SOLID, with examples you’d actually see

30-second answer: five principles that keep object-oriented code easy to change. They cut coupling, so a change in one place doesn’t ripple through the system.

PrincipleIn one lineSmells likeFix
Single responsibilityOne reason to changeOrderService validates, prices, saves and sends SMSOrderValidator, PricingService, OrderRepository, Notifier
Open/closedExtend, don’t modifyA switch (paymentType) you edit for every new methodA PaymentProcessor interface; new method = new class
Liskov substitutionSubtypes work wherever the parent doesSquare extends Rectangle breaks setWidthModel by behaviour; separate types
Interface segregationDon’t force unused methodsWorker with work() and eat() that Robot must implementSmall role interfaces
Dependency inversionDepend on abstractionsnew MySqlOrderRepo() inside the serviceInject an OrderRepository

Open/closed and dependency inversion together look like this:

JAVA
interface PaymentProcessor {
    PaymentResult pay(Order o);
    boolean supports(PaymentMode m);
}

class CheckoutService {
    private final List<PaymentProcessor> processors;   // injected

    CheckoutService(List<PaymentProcessor> processors) { this.processors = processors; }

    PaymentResult pay(Order o) {
        return processors.stream()
            .filter(p -> p.supports(o.mode()))
            .findFirst().orElseThrow()
            .pay(o);                                   // new method = new class, not a new branch
    }
}
// FOLLOW-UP: CAN SOLID BE OVERDONE?

Yes. A dozen tiny classes and single-implementation interfaces add indirection with no benefit. Apply the principles where change is actually likely.

02

Composition vs inheritance

30-second answer: inherit only for a true is-a relationship that honours the parent’s contract. Otherwise compose (has-a): it’s more flexible, swappable at runtime, and avoids fragile base classes.

Why inheritance bites:

  • Fragile base class: a change in the parent silently breaks children.
  • Class explosion: one class per combination of features.
  • One axis only: single inheritance forces you to pick a single dimension of reuse.
JAVA
// Inheritance: one class per combination      // Composition: combine small parts
class ExpressFragileShipment extends ...       class Shipment {
class ExpressStandardShipment extends ...          DeliveryPolicy delivery;   // Express | Standard
class EconomyFragileShipment extends ...           PackagingPolicy packaging; // Fragile | Standard
                                               }

When inheritance is genuinely right: framework template methods, sealed type hierarchies for domain modelling (sealed interface PaymentEvent permits ...), and genuinely shared invariants. Naming one of these is what turns “favour composition” from a slogan into an answer.

03

Patterns you’ll actually use

30-second answer: Strategy for swappable algorithms, Factory to pick an implementation, Builder for complex immutable objects, Observer for domain events, Decorator to add retries, caching or metrics around a client, Adapter to wrap third-party SDKs.

FamilyPatternTypical use
CreationalFactoryPick a NotificationSender (SMS, push, email) by channel
CreationalBuilderRequest DTOs, test fixtures
StructuralAdapterHide a payment provider’s SDK behind your own PaymentGateway
StructuralDecoratorCachingCatalogClient wraps CatalogClient
StructuralFacadeOne CheckoutFacade over cart, pricing and payment
BehaviouralStrategyDiscount, ETA or matching algorithms
BehaviouralObserverOrderPlaced → notifications, analytics, loyalty
BehaviouralStateOrder lifecycle: CREATED → CONFIRMED → SHIPPED → DELIVERED
BehaviouralChain of responsibilityValidation pipelines, middleware
JAVA
// Decorator: caching without touching the real client
class CachingCatalogClient implements CatalogClient {
    private final CatalogClient delegate;
    private final Cache<String, Product> cache;

    public Product get(String id) {
        return cache.get(id, delegate::get);
    }
}
// FOLLOW-UPS

Why is Singleton called an anti-pattern? Hidden global state, hard to mock, a concurrency risk. Let the DI container manage singleton scope instead.

State vs Strategy? With State, the object moves itself between states. With Strategy, the caller picks the behaviour.

04

Dependency injection

30-second answer: a class receives its collaborators from outside, usually through the constructor, instead of creating them. That decouples it from concrete implementations, makes unit tests trivial, and centralises wiring and lifetimes.

  • Prefer constructor injection: dependencies are explicit, fields can be final, the object is never half-built. Avoid field injection; it hides dependencies.
  • Scopes: singleton, prototype, request.
  • Warning sign: a constructor with eight dependencies is a class doing too much.
JAVA
class OrderService {
    private final OrderRepository repo;
    private final EventPublisher events;

    OrderService(OrderRepository repo, EventPublisher events) {
        this.repo = repo;
        this.events = events;
    }
}
// In a test: new OrderService(new InMemoryOrderRepo(), new FakePublisher());

Circular dependencies (A needs B, B needs A) are usually a design smell. Extract a third component, or talk through events.

05

Interface vs abstract class

30-second answer: an interface is a contract (what something can do), and a class can implement many. An abstract class shares state and partial implementation among close relatives, and you can extend only one. Since interfaces can have default methods, the real question is state: need shared fields or constructor logic? Abstract class. Otherwise, interface.

InterfaceAbstract class
Multiple inheritanceYesNo
Instance fieldsNo (constants only)Yes
ConstructorsNoYes
Use forCapabilities, API boundariesTemplate method, shared base logic

Program to interfaces at module boundaries (repositories, clients, gateways). That’s what makes swapping implementations and testing cheap.

06

Why immutability matters

30-second answer: immutable objects are thread-safe without locks, safe to share and cache, safe as map keys, and easy to reason about. Make the class final, fields private final, no setters, copy mutable inputs, and return new instances for “changes”.

JAVA
public final class Money {
    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public Money plus(Money other) {
        return new Money(amount.add(other.amount), currency);
    }
}
// Java 16+: record Money(BigDecimal amount, Currency currency) {}
// Records are shallowly immutable: still copy lists, e.g. List.copyOf(items)
// FOLLOW-UPS

Cost? More allocations, usually negligible with modern GCs.

Why must hash-map keys be immutable? If a field in hashCode changes after insertion, the entry sits in the wrong bucket forever.

Why BigDecimal for money? Binary floating point can’t represent 0.1 exactly. Use BigDecimal or integer cents.

07

Coupling, cohesion, and spotting bad design

30-second answer: cohesion is how related the things inside a module are (you want it high). Coupling is how much one module depends on another’s internals (you want it low). Together they keep a change local.

Smells to name in a review:

  • Shotgun surgery: one change touches many files or services.
  • God class: thousands of lines, dozens of dependencies.
  • Feature envy: a method that uses another class’s data more than its own.
  • Leaky abstraction: database entities returned straight from the API.
  • Distributed monolith: services that must deploy together or share a database.
// SAY IT LIKE THIS

When reviewing a design I ask two questions. If this requirement changes, how many places do we touch? And can I test this piece without spinning up the whole world?

08

Cheat sheet

  • S one reason to change · O extend, don’t edit · L substitutable subtypes · I small interfaces · D depend on abstractions.
  • Composition by default; inheritance for true is-a or sealed hierarchies.
  • Strategy, Factory, Builder, Decorator, Adapter, Observer, State: always inside a story.
  • Constructor injection; many dependencies = too many responsibilities.
  • Interface unless you need shared state.
  • Immutable value objects; BigDecimal or cents for money.
→

More in Technical fundamentals

Note 1
OOP & design principles
You're reading this.
Note 2
APIs & the web
Note 3
Databases
Note 4
Caching & performance
Note 5
Concurrency & the OS
Note 6
Networking & distributed systems
Note 7
Security, DevOps & architecture
END OF NOTE 1

Up next: APIs & the web

Short, plain-language notes on what interviewers ask an SDE2 and how to think about it.

Contact