Four things vary, so four seams
Before drawing a class, say out loud what changes independently. A new channel such as WhatsApp should not touch the code that decides who receives what. A copy change to the "order shipped" email should not need a deploy of sending code. A user switching off SMS should not alter any channel. A provider that starts rate-limiting should change how you retry, not what you send.
Each of those sentences describes a seam, and the classes fall out of them. The interviewer is scoring whether the structure follows from the requirements, so the order matters. Name the axes first, then show the class that absorbs each one.
Channels are the one clear strategy
Sending is the part that differs per channel and has the same shape for every caller, which is exactly when the strategy pattern earns its place. The strategy versus inheritance argument applies directly: the orchestrator holds a reference to a channel rather than being a subclass of one.
import java.time.Duration;
import java.util.*;
enum ChannelType { EMAIL, SMS, PUSH }
record Recipient(String userId, String email, String phone, List<String> deviceTokens) {}
record Rendered(String subject, String body) {}
sealed interface SendResult permits Sent, Transient, Permanent {}
record Sent(String providerMessageId) implements SendResult {}
record Transient(String reason) implements SendResult {}
record Permanent(String reason) implements SendResult {}
interface Channel {
ChannelType type();
SendResult send(Recipient to, Rendered message, String dedupeKey);
}
Each implementation wraps one provider client and translates its error codes into Transient or Permanent. That translation belongs in the channel because only the channel knows that a given provider response means "number does not exist". The channels are built once at startup into a Map<ChannelType, Channel> and injected, which is the registry approach from one object per message type. A separate ChannelFactory class adds nothing that map does not already do.
Templates are data, not a class per notification
The tempting mistake is an OrderShippedEmail class, an OrderShippedSms class and so on, which multiplies notification types by channels. The text is content, not behaviour. Store templates keyed by notification type, channel and locale, render them with the event's data, and let product staff change copy without a code review. SMS and push need short bodies and email needs a subject, so the channel decides which template fields it requires.
Watch the vocabulary here. A message template is not the template method pattern, and conflating the two in an interview suggests the pattern names were memorised rather than understood.
Preferences and retry are policies
PreferencePolicy answers one question: for this user and this kind of notification, which channels? It is where opt-outs, quiet hours and "security alerts ignore preferences" rules live, and it knows nothing about sending. RetryPolicy owns backoff and the attempt limit. Retries go back on a queue with a delay instead of sleeping inside the sending thread, so one slow provider cannot hold a worker hostage. A retried attempt runs deliver again, so a user who opted out while the retry waited should be checked again at that point. The sketch leaves that check out.
The dedupe key matters most when a provider times out after it may already have delivered. Passing a stable key lets a provider that supports idempotent requests drop the duplicate, and the delivery log lets you refuse a resend you already recorded as sent. Queues per channel, provider failover and rate limits at volume are the system-design version, covered in fifty million notifications a day.
Counting patterns is not the score
The weak answer to this question is a list of design patterns: factory, abstract factory, builder, singleton, observer, decorator, chain of responsibility. Each one may be defensible alone, and together they produce a class diagram bigger than a service that sends three kinds of message. The interviewer is scoring judgement, and judgement shows up as refusals with reasons.
Leave out abstract factory, because there is no family of related objects that must be created together. Leave out a builder for Notification, because a record with five fields is already readable. Avoid a singleton service, because injection gives you one instance without the global state (singleton and the reasons to avoid it). A chain of responsibility for preferences turns three readable rules into a debugging exercise about which handler stopped the chain.
Two patterns do earn a conditional place. A decorator that wraps a Channel to add metrics or a circuit breaker is worth it once you have that need, because it keeps cross-cutting code out of every provider class. Observer is how the rest of the system reaches this service: an OrderShipped event is published and the notification module subscribes, with the caveats in observer and the problems it quietly creates. Introduce each only when a requirement asks for it, and say which requirement.
Name the four axes, give each one seam, and refuse the patterns that do not remove a problem. The strategy pattern for channels is the only design pattern this notification service needs on day one.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).