OOP Design Scenarios: Composition, Interfaces and Abstract Classes
Learn to defend OOP design boundaries through two complete examples: a multi-channel incident alert service and a report exporter with independent formats and destinations.
KnowledgeGate Team
Exam prep & CS education

Defining classes is usually easy. The harder moment comes when two designs both compile and the interviewer asks, "Why this boundary?" In an incident-alert service, composition keeps email and SMS replaceable behind one delivery contract. In a report-export service, separate renderer and destination contracts prevent a subclass grid. For broader preparation, use the placement interview preparation, while treating company-specific interview formats as variable.
1. Use four questions before choosing an OOP relationship
Ask these questions in order:
Is the relationship genuinely "is-a", and can the child substitute for the parent without surprising the caller?
Is one object merely using another behaviour, which makes the relationship "has-a"?
Must otherwise unrelated classes promise the same capability?
Is there stable shared implementation or state across a real family?
True substitutability can justify inheritance. A replaceable collaborator points to composition. A capability contract points to an interface. A stable family with shared invariants can justify an abstract class.
Abstraction itself is the act of exposing what callers need while hiding the mechanism. It is not another name for an abstract class. The one-line test is: prefer the smallest dependency that preserves the required behaviour. If classes, inheritance, encapsulation, and polymorphism still feel mixed together, revise OOP concepts for teaching and CS exams before applying this sequence.
2. Scenario 1: reject the inheritance-shaped alert service
The prompt is precise: design an AlertService that sends alert INC-17, severity P1, text API latency above 2 seconds, to email recipient ops@example.com and SMS recipient +91-90000-00000. Email may retry up to 3 times with a 2,000 ms timeout. SMS currently uses the same limits.
Consider three sketches:
AlertService extends EmailSenderfails immediately. An alert service is not an email sender, so the direction fails the is-a test. Adding SMS also creates pressure for awkward multiple roles.AlertServicecontaining concreteEmailSenderandSmsSenderplaces responsibilities better. However, the service still depends on concrete classes and changes whenever a channel is added.AlertServicecontainingList<Channel>is the cleaner choice. Delivery is replaceable, and the service requires only thesendcontract.
The interview-ready justification is: "I used composition because the service has delivery channels, and an interface because email and SMS provide the same capability without forming one natural data hierarchy."
3. Work the alert design from input to result
The design can stay small and explicit:
record Alert(String id, String severity, String text) {}
record Recipient(String value) {}
interface Channel {
DeliveryResult send(Alert alert, Recipient recipient);
}
final class AlertService {
private final List<Channel> channels;
private final Map<Channel, Recipient> recipients;
AlertService(List<Channel> channels,
Map<Channel, Recipient> recipients) {
this.channels = List.copyOf(channels);
this.recipients = Map.copyOf(recipients);
}
List<DeliveryResult> notify(Alert alert) {
return channels.stream()
.map(channel -> channel.send(alert, recipients.get(channel)))
.toList();
}
}EmailChannel and SmsChannel implement Channel. Constructor injection supplies both channel objects and their recipient mapping, so AlertService does not instantiate either adapter.
Now run the design. Create Alert("INC-17", "P1", "API latency above 2 seconds"). Configure email with maxAttempts=3, timeoutMs=2000, and ops@example.com. Configure SMS with the same limits and +91-90000-00000. The illustrative email adapter fails attempt 1, retries, and succeeds on attempt 2. SMS succeeds on attempt 1. The two returned results, shown as tuples rather than Java's printed record syntax, are [(EMAIL, SENT, 2), (SMS, SENT, 1)].
Adding PushChannel changes construction and configuration, not AlertService. If every channel later needs the same stable retry control, that mechanism may move into an abstract base. Two matching constants today are not enough evidence for that family.

4. Scenario 2: stop a subclass explosion in report export
The second prompt is to export report R-42, containing exactly 1,250 rows, in CSV, PDF, or HTML, then deliver it by EMAIL or OBJECT_STORE.
A subclass-per-combination design needs CsvEmailReport, CsvObjectStoreReport, PdfEmailReport, PdfObjectStoreReport, HtmlEmailReport, and HtmlObjectStoreReport. Three formats multiplied by two destinations gives 3 x 2 = 6 subclasses. Add JSON and LOCAL_DISK, and the grid becomes four formats multiplied by three destinations, or 4 x 3 = 12 combinations. Independent choices are being forced into one hierarchy.
Separate them behind Renderer.render(ReportData): Artifact and Destination.deliver(Artifact): Receipt. ReportExportService receives one renderer and one destination through composition, calls them in sequence, and knows neither file-format details nor delivery mechanics.
For an exact run, ReportJob(id="R-42", rowCount=1250) uses CsvRenderer and ObjectStoreDestination. Rendering produces an artifact with contentType="text/csv". Delivery returns a receipt with path="reports/R-42.csv" and status="STORED". Switching to PdfRenderer leaves delivery code untouched. Switching to EmailDestination leaves rendering code untouched.

5. Use an abstract class only when the shared mechanism is stable
Return to alerts. abstract class RetryingChannel implements Channel can make sense when retry is a genuine family invariant. Its final send template owns the attempt loop, enforces maxAttempts=3, and calls a protected doSend implemented by EmailChannel or SmsChannel. Subclasses vary only the transport step, while the base guarantees the shared sequence.
Do not force this hierarchy merely to remove duplicated lines. If email later needs batching while SMS uses asynchronous delivery or a different retry meaning, one fixed template becomes a liability. Keep the Channel interface and move retry into a composed RetryPolicy that can vary independently at runtime.
Say the rule aloud: choose an abstract class for shared code plus a true family invariant; choose an interface for a capability; compose policies when behaviour must vary independently.
6. Put responsibilities where their reasons to change live
Audit the report solution by asking what fact could make each part change:
Part | Responsibility |
|---|---|
| Owns the 1,250 rows and report metadata |
| Owns conversion into CSV, PDF, HTML, or another format |
| Owns email transport or object storage |
| Coordinates render, then deliver |
The service must not encode CSV, open network connections, or decide retry timing.
Three traps follow from ignoring those boundaries. A GodService does everything, so repair it by separating policies and adapters. A marker interface promises nothing useful, so define the operation the caller actually needs. A deep inheritance tree exists only to reuse code, so extract the changing behaviour into a collaborator.
Immutable values such as Alert and a delivery Receipt also make boundaries easier to reason about. Their state cannot shift halfway through a call. That is a useful local design choice, not proof that every domain object must be immutable.
7. How to present the design in a short technical interview
Use this eight-minute answer shape:
Spend 1 minute restating requirements and identifying what varies.
Spend 2 minutes naming responsibilities.
Spend 2 minutes sketching small interfaces and composition.
Spend 2 minutes running a concrete input such as
INC-17.Spend 1 minute stating one trade-off and one extension point.
Narrate constraints before naming patterns: "Format and destination vary independently, so inheritance creates combinations. I will compose a renderer and destination behind small interfaces." This shows why the design exists.
The interviewer can then probe substitutability, test doubles, constructor injection, shared state, or the cost of adding one variant. For the other subject areas that may support such discussions, use the technical interview CS subjects for freshers revision map. Do not memorise a diagram without being ready to explain its trade-offs.
8. The short version and the next practice step
Use inheritance for a stable is-a relationship that preserves substitutability. Use composition for has-a relationships and replaceable behaviour, interfaces for capability contracts, and abstract classes for a true family with stable shared mechanics.
Redraw both designs from memory. Add PushChannel to INC-17, then add JSON to R-42, and note which existing classes change. Continue technical practice with Coding for Placements, then practise mock interviews and answer delivery through the Interview & Resume Preparation Course.
Keep learning

Placement Mock Analysis: One Error Ledger Across Every Test Round
Use one error ledger without flattening unlike round results. This worked example shows how to find the first wrong step, prioritise repairs and close errors only after fresh retests.

Internship to PPO: Build a Weekly Evidence Trail Before the Final Review
Use a weekly outcome ledger to make your internship work visible before the final review. This practice model shows how to record delivery, feedback, effect and handoff honestly.

DSA Mock Interview Rubric: A 100-Point Scorecard for Reasoning, Code and Communication
A practical six-part scorecard for running comparable DSA mocks, grading visible evidence and turning weak areas into the next week's practice.

Campus Recruitment Timeline: Stage by Stage from Pre-Placement Talk to Written Offer
Follow a campus drive without guessing. Build an evidence sheet, verify eligibility, plan each preparation window and check the written offer before responding.