OOPS Interview Questions · Question 23

A notification service supports Email and SMS. How would you add Push Notification without filling the client code with conditionals?

Interview preparation resource from Gate Smashers.

Interview-ready answer

Use polymorphism: define a NotificationSender interface, provide EmailSender, SmsSender, and PushSender implementations, and have NotificationService obtain the appropriate sender from an injected registry or factory. Client code calls the service with a channel key and notification; it does not contain Email/SMS/Push conditionals. To add Push, implement PushSender and register it during application configuration.

Java examplejava
import java.util.HashMap;
import java.util.Map;

public final class NotificationExample {
    public interface NotificationSender {
        void send(Notification notification);
    }

    public static final class Notification {
        private final String recipient;
        private final String subject;
        private final String body;

        public Notification(String recipient, String subject, String body) {
            this.recipient = recipient;
            this.subject = subject;
            this.body = body;
        }

        public String recipient() {
            return recipient;
        }

        public String subject() {
            return subject;
        }

        public String body() {
            return body;
        }
    }

    public static final class EmailSender implements NotificationSender {
        @Override
        public void send(Notification notification) {
            // Send through an email provider.
        }
    }

    public static final class SmsSender implements NotificationSender {
        @Override
        public void send(Notification notification) {
            // Send through an SMS provider.
        }
    }

    public static final class PushSender implements NotificationSender {
        @Override
        public void send(Notification notification) {
            // Map the notification to a push payload and send it.
        }
    }

    public static final class SenderRegistry {
        private final Map<String, NotificationSender> senders = new HashMap<>();

        public void register(String channelKey, NotificationSender sender) {
            senders.put(channelKey, sender);
        }

        public NotificationSender resolve(String channelKey) {
            NotificationSender sender = senders.get(channelKey);
            if (sender == null) {
                throw new IllegalArgumentException("Unsupported channel: " + channelKey);
            }
            return sender;
        }
    }

    public static final class NotificationService {
        private final SenderRegistry registry;

        public NotificationService(SenderRegistry registry) {
            this.registry = registry;
        }

        public void send(String channelKey, Notification notification) {
            registry.resolve(channelKey).send(notification);
        }
    }

    public static void main(String[] args) {
        SenderRegistry registry = new SenderRegistry();
        registry.register("email", new EmailSender());
        registry.register("sms", new SmsSender());
        registry.register("push", new PushSender());

        NotificationService service = new NotificationService(registry);
        service.send("push", new Notification("recipient", "Title", "Body"));
    }
}
Understand it clearly

Use a common abstraction

Define an interface such as NotificationSender with a send(Notification) method. Each delivery channel implements this interface and owns its transport-specific behavior.

NotificationService depends on the abstraction and delegates sending to the selected implementation. This is a Strategy-style use of polymorphism: the service invokes the same operation regardless of whether the selected sender is email, SMS, or push.

  • EmailSender: Sends notifications through the email transport.
  • SmsSender: Sends notifications through the SMS transport.
  • PushSender: Builds and sends the push-specific payload through the push transport.

Resolve the sender outside client code

Inject a registry, factory, or dependency-injection configuration into NotificationService. A registry can map channel keys such as email, sms, and push to NotificationSender instances.

The client calls NotificationService.send(channelKey, notification). The service resolves the sender and delegates to it, so the client remains independent of concrete channel classes and contains no branching logic.

  • Registry: Appropriate when sender instances are configured at startup and selected by a key.
  • Factory: Useful when sender creation itself depends on configuration or request-specific context.
  • Dependency injection: Can construct and supply the registry or sender implementations during application setup.

Add Push without changing callers

Create PushSender, implement the common interface, and register it under the push key in application wiring. Existing client code and the NotificationService delegation logic do not need to change.

This supports the Open/Closed Principle in practice: behavior is extended by adding a new implementation and configuration rather than editing client-side conditional branches.

  • Validation: The resolver should reject or otherwise handle an unsupported channel key.
  • Channel-specific data: Keep payload mapping, credentials, provider calls, and delivery constraints inside the relevant sender or its collaborators.

Operational boundaries

A shared Notification model should contain only data common to callers. Each sender can translate that model into the format required by its transport. If a channel needs additional data, model that requirement explicitly rather than assuming every channel accepts identical fields.

Retry behavior, observability, and fallback rules should be placed deliberately. A sender may handle transport-level failures, while a separate orchestration policy can decide whether a failed push should trigger another channel.

  • Testing: Unit-test NotificationService with a test registry or test sender, and test each concrete sender independently.
  • Configuration: Inject provider-specific configuration into sender implementations rather than exposing it to clients.