Skip to content
Preptima

Low-level design fundamentals

What encapsulation is actually protecting, each SOLID letter stated as a consequence rather than a slogan, the patterns worth naming and the ones worth refusing, and how a live parking-lot question is scored. Sixty items, sixteen worked through with code, a table or a diagram.

60 questions

Go deeper on Low-Level Design & OOD
Standard Sheet View

Objects and responsibilities

What is encapsulation actually protecting?

Not the fields — the invariants that relate them. A class with private fields and a getter and setter for each one is exactly as exposed as a struct, because any caller can still put it into a state it should never occupy. Encapsulation means the object owns the rules about what combinations of values are legal, so an Order refuses to add a line once it is dispatched rather than trusting every call site to check first. That reframing is what makes the follow-up answerable: the question is never "are the fields private" but "which statements about this object are always true, and what code could break one".

Why is composition preferred over inheritance?

Because inheritance couples you to the superclass's implementation, not just its interface, and that coupling is permanent and invisible from the subclass's own file. Composition gives you the same reuse through delegation while leaving the relationship explicit and replaceable at runtime, which is also what makes the collaborator injectable in a test. The rule of thumb is that inheritance models "is a permanent kind of" and composition models "has a" or "uses a", and most things candidates model as inheritance are really varying behaviour — which is a strategy held by reference, not a subclass. The cost of composition is more wiring and one more indirection to read through.

What is the fragile base class problem?

A superclass cannot be changed safely once subclasses exist, because subclasses may depend on which of its own methods it calls internally. If Collection.addAll happens to loop over add, a subclass that overrode both to count elements double-counts, and the bug appears when the base class is refactored rather than when the subclass is written. Nothing in the compiler or the tests of either class catches it, since each is correct in isolation. The mitigations are to document self-calls as part of the contract, to make classes final unless designed for extension, or to avoid the inheritance and delegate instead.

Show me a god class refactored into responsibilities.

The class works and the tests pass. The problem is that every change touches it.

Before: OrderService, 1,400 lines, 31 public methods

  validates the cart          -> business rules
  applies discount codes      -> pricing rules
  calls the payment gateway   -> external integration
  writes rows to three tables -> persistence
  renders the receipt email   -> presentation
  publishes an audit event    -> messaging
  formats currency for the UI -> formatting

Reasons this class changes: a tax rate, a gateway API version, a schema
migration, a copy edit in the email, a new audit field. Five unrelated teams,
one file, one merge conflict.

The useful test is not size, it is the number of reasons to change. Every distinct actor who can force an edit is a separate responsibility, and a class with five of them will be edited five times as often as anything else in the codebase, which is where the merge conflicts and the accidental regressions come from.

The refactoring is mechanical once you have that list. PricingPolicy owns discounts and tax. PaymentGateway is an interface with one adapter per provider. OrderRepository owns persistence. ReceiptMailer owns the email. OrderService keeps only the orchestration — the sequence of steps, the transaction boundary, and what happens when a step fails.

The detail candidates miss is what the orchestrator is allowed to keep. Splitting a god class into six anaemic data holders plus a service that still does all the thinking has moved the problem, not solved it: the behaviour belongs with the data it constrains, so the discount calculation goes into PricingPolicy with the rates, not into a static helper the service calls.

The measurable win is testability. Pricing rules become a pure unit test with no gateway, no database and no mail server, and that is usually the moment the team discovers which of the rules nobody could previously state.

What is cohesion, and how do you tell a class has too little?

Cohesion is how much the members of a class belong together — whether the methods operate on the same state toward the same purpose. The practical signal of low cohesion is that different methods touch disjoint subsets of the fields, so you can draw a line through the class and separate it without breaking anything. A second signal is the name: if the only honest name is Manager, Helper, Utils or Processor, the class has no single concept, because you were unable to say what it is. Low cohesion matters because it drags unrelated reasons to change into one file.

What does "tell, don't ask" mean in practice?

Send an object a message describing intent rather than extracting its state, deciding elsewhere, and pushing state back. account.withdraw(amount) keeps the overdraft rule inside the account; if (account.getBalance() >= amount) account.setBalance(account.getBalance() - amount) copies that rule to every call site and guarantees one of them will disagree. The deeper point is that getters-plus-external-logic is how behaviour leaks out of a class and how the same validation ends up implemented three times slightly differently. It is not absolute — reporting and serialisation legitimately need to read state — but a class whose entire surface is accessors has no behaviour to protect.

What is the law of Demeter, and when is it worth breaking?

A method should only talk to its own fields, its parameters, and objects it created — not to things reached by navigating through them. The symptom is a chain like order.getCustomer().getAddress().getCountry().getTaxRate(), which couples the caller to four classes and breaks when any link changes shape or returns null. The fix is usually to move the behaviour rather than the navigation, asking order.taxRate() instead. It is worth breaking for fluent builders and query DSLs, where each call returns the same conceptual object rather than reaching into a stranger's internals, and for plain data structures that have no invariants to protect.