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

Updated 17 Sep 20266 min read

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:

  1. Is the relationship genuinely "is-a", and can the child substitute for the parent without surprising the caller?

  2. Is one object merely using another behaviour, which makes the relationship "has-a"?

  3. Must otherwise unrelated classes promise the same capability?

  4. 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:

  1. AlertService extends EmailSender fails 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.

  2. AlertService containing concrete EmailSender and SmsSender places responsibilities better. However, the service still depends on concrete classes and changes whenever a channel is added.

  3. AlertService containing List<Channel> is the cleaner choice. Delivery is replaceable, and the service requires only the send contract.

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:

java
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.

AlertService composed with a list of channels, delivering alert INC-17 by email and SMS with per-channel retry results.

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.

A subclass-per-combination grid contrasted with ReportExportService composing one renderer and one destination for report R-42.

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

ReportData

Owns the 1,250 rows and report metadata

Renderer

Owns conversion into CSV, PDF, HTML, or another format

Destination

Owns email transport or object storage

ReportExportService

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:

  1. Spend 1 minute restating requirements and identifying what varies.

  2. Spend 2 minutes naming responsibilities.

  3. Spend 2 minutes sketching small interfaces and composition.

  4. Spend 2 minutes running a concrete input such as INC-17.

  5. 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.