What are the four pillars of OOP, and what is the difference between abstraction and encapsulation?
Encapsulation, inheritance, polymorphism and abstraction are worth stating as consequences rather than definitions - what each one buys you and what it costs. Abstraction decides what a type means to its caller; encapsulation protects how that meaning is upheld.
What the interviewer is scoring
- Does the candidate describe each pillar by what it changes about a design rather than by its definition
- Whether encapsulation is tied to protecting an invariant rather than to writing getters and setters
- That inheritance is named as the tightest coupling available, with composition offered as the default
- Whether polymorphism is connected to adding cases without editing existing code
- Does the candidate separate abstraction as a design choice from encapsulation as a mechanism
Answer
Answer with consequences, not definitions
This question is asked verbatim in enough screens that most candidates have a rehearsed paragraph for it, which is precisely why the rehearsed paragraph scores badly. Four definitions delivered fluently tell the interviewer you have read a tutorial. What distinguishes a good answer is stating, for each pillar, what it lets you change about a design and what you give up by using it. That framing survives follow-ups, because every follow-up here is a why or a when not.
Encapsulation buys you a place to enforce an invariant. Because the object owns its state and controls the paths that mutate it, there is exactly one region of code responsible for the rule that a balance never goes negative, or that a date range never has its end before its start. The consequence is that when the rule is violated, you know where to look. Give up encapsulation and the invariant becomes a convention distributed across every caller, which is a different way of saying it is not enforced.
Abstraction buys you the freedom to change the implementation without renegotiating with callers. Naming a PaymentGateway with a charge operation means the callers depend on the idea of charging, not on an HTTP client and a retry policy. The cost is real and usually underweighted: every abstraction is a guess about which axis will vary, and a wrong guess is worse than no abstraction, because now the change you need is the one the interface forbids.
Inheritance buys you shared implementation and a subtype relationship in one move. It is also the tightest coupling any mainstream language offers — a subclass can depend on the order in which its parent calls its own methods, so a behaviour-preserving change in the parent can break it. That is the fragile base class problem, and it is why the sensible default is composition, with inheritance reserved for the case where the subtype genuinely is substitutable for the parent everywhere the parent is used.
Polymorphism buys you the ability to add a case without editing the code that consumes it. A new Shape implementation requires no change to the renderer; a new payment provider requires no change to the checkout. The cost is that control flow stops being locally readable. A reader looking at shape.area() cannot tell which code runs without knowing what was constructed, and that indirection is a genuine debt when there are only two implementations and no third is coming.
Abstraction versus encapsulation
These get conflated because both are described as "hiding", and the sentence "abstraction hides implementation, encapsulation hides data" is repeated everywhere without clarifying anything. The useful separation is that they operate at different stages.
Abstraction is a design decision about what the type is. You decide that the concept your callers need is a queue with push and pop, that whether it is backed by an array or a linked list is not part of the concept, and that ordering is. Encapsulation is the mechanism by which the code makes that decision hold: private fields, no leaked mutable references, mutation only through operations that maintain the invariant. Abstraction is what you chose to expose; encapsulation is why the choice cannot be circumvented.
Two consequences follow. You can have abstraction without encapsulation — a well-named interface whose implementation stores its state in a public static map — and it will be violated within a release. And you can have encapsulation without abstraction, which is the more common failure:
// Encapsulated in form, not in substance. Every field is reachable and
// settable, so no invariant lives here and the private keyword buys nothing.
public class Order {
private List<Item> items;
public List<Item> getItems() { return items; } // caller can mutate the list
public void setTotal(BigDecimal t) { this.total = t; }
}
// The invariant - total always equals the sum of items - now has exactly one
// owner, and there is no path that can leave the object inconsistent.
public class Order {
private final List<Item> items = new ArrayList<>();
private BigDecimal total = BigDecimal.ZERO;
public void add(Item item) {
items.add(item);
total = total.add(item.price()); // the only place total changes
}
public List<Item> items() { return List.copyOf(items); } // defensive copy
public BigDecimal total() { return total; }
}
The first class is what most codebases mean by encapsulation, and it is a data holder with ceremony. The second one holds a rule.
Where a fluent answer still fails
The failure is not getting a definition wrong. It is answering entirely in the language of access modifiers — encapsulation is private, abstraction is an abstract class or an interface — because that reduces four design ideas to four keywords in one language, and it collapses immediately when the interviewer asks about a language without them. Encapsulation in C is a struct whose definition lives in the .c file and whose header exposes only functions taking an opaque pointer; there is no private anywhere and the invariant is protected. In Python nothing is truly private, and encapsulation is upheld by convention plus the module boundary. If your answer cannot survive being moved to either language, it was a description of Java syntax rather than of object orientation.
The related tell is being unable to say when a pillar is wrong. Anyone who has designed something has an inheritance hierarchy they regret, and saying so — the second level that turned out to need only half of what it inherited, the abstraction added for a second implementation that never arrived — is worth more than the four definitions delivered perfectly.
Likely follow-ups
- Show me a class with public getters and setters for every field and tell me whether it is encapsulated.
- When is inheritance the right choice over composition?
- What does the Liskov substitution principle rule out that the compiler allows?
- Where does polymorphism make code harder to read rather than easier?
Related questions
- Where does encapsulation or polymorphism change a design, and how do you decide between composition and inheritance?mediumAlso on encapsulation and polymorphism6 min
- What is the dependency inversion principle, and what exactly is being inverted?mediumAlso on abstraction3 min
- When you call an overridden method through a base-class reference, how does the runtime know which implementation to run?mediumAlso on polymorphism5 min
- When would you use the strategy pattern instead of inheritance?mediumAlso on inheritance5 min
- How do you split a story that is too big to fit in a Sprint?mediumSame kind of round: concept3 min
- What is the Definition of Done, who owns it, and what do you do with an item that misses it at the end of a Sprint?mediumSame kind of round: concept3 min
- Take this table to third normal form, then tell me when you would deliberately denormalise — and what does ACID guarantee?mediumSame kind of round: concept5 min
- Walk me through Scrum as the Guide defines it: the accountabilities, the events, the artefacts, and what each event is for.easySame kind of round: concept5 min