Refactoring 7 Duplicate Notification Services Using Template Method Pattern in Spring Boot

The article demonstrates how to correctly apply the DRY principle by refactoring seven duplicated notification service classes in Spring Boot 3.5.0 using the template method pattern and a NotificationMessage value object, consolidating 40 lines of repeated validation, logging, and error handling into a single abstract base class.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Refactoring 7 Duplicate Notification Services Using Template Method Pattern in Spring Boot

Problem: Copy-Paste Duplication Across Notification Services

The article begins with a clean EmailNotificationService that validates order and user, logs, builds a message, sends via an email client, and handles errors. When SMS notifications are required, the class is copied and slightly modified — changing the contact field to phone number, the client to SmsClient, and log messages. This pattern repeats for push, WeChat, in-app, and other channels, resulting in seven services each containing roughly 40 lines of nearly identical code.

The duplication is not merely syntactic; it represents the same business knowledge: "an order notification requires a valid order and a valid user with a valid contact method." When this rule changes, all seven copies must be updated, and missing one leads to inconsistent production behavior.

Understanding DRY Correctly

DRY (Don't Repeat Yourself) is often misunderstood as avoiding duplicate lines of code. The authors (Andy Hunt and Dave Thomas) define it as ensuring every piece of knowledge has a single, authoritative, unambiguous representation. If two methods share a three-line validation but represent different business concepts, extracting them creates harmful coupling. However, when the same validation appears in seven notification services and embodies the same rule, each copy is redundant.

Refactoring Step 1: Extract a Common Abstract Base Class

The invariant parts across all services are:

Validate order and user

Validate channel-specific contact method

Log send attempt

Build order message

Send via channel

Log success or failure

Wrap errors in a domain exception

The variant parts are:

Specific contact field (email, phone, device token)

Specific send mechanism (email client, SMS client, push client)

Minor log message details (channel name)

This is a classic Template Method pattern scenario. The overall algorithm is fixed; only the "validate contact" and "deliver" steps differ.

Abstract Base Class

public abstract class NotificationService {
  private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
  public void sendOrderNotification(Order order, User user) {
    validateOrderAndUser(order, user);
    validateContactMethod(user);
    log.info("正在为用户 {} 发送订单 {} 的通知", user.getId(), order.getId());
    String message = buildOrderMessage(order, user);
    try {
      deliver(user, message);
      log.info("订单 {} 的{}发送成功", order.getId(), getChannelName());
    } catch (Exception e) {
      log.error("订单 {} 的{}发送失败", order.getId(), getChannelName(), e);
      throw new NotificationException(getChannelName() + "发送失败", e);
    }
  }
  private void validateOrderAndUser(Order order, User user) {
    if (order == null) {
      throw new IllegalArgumentException("订单不能为空");
    }
    if (user == null) {
      throw new IllegalArgumentException("用户不能为空");
    }
  }
  private String buildOrderMessage(Order order, User user) {
    return String.format("您好 %s,您的订单 #%s 已收到,总金额:%s",
      user.getName(), order.getId(), order.getTotalAmount());
  }
  protected abstract void validateContactMethod(User user);
  protected abstract void deliver(User user, String message);
  protected abstract String getChannelName();
}

Concrete Implementations

Each concrete service now implements only three small methods:

public class EmailNotificationService extends NotificationService {
  private final EmailClient emailClient;
  public EmailNotificationService(EmailClient emailClient) {
    this.emailClient = emailClient;
  }
  @Override
  protected void validateContactMethod(User user) {
    if (user.getEmail() == null || user.getEmail().isBlank()) {
      throw new IllegalArgumentException("用户邮箱不能为空");
    }
  }
  @Override
  protected void deliver(User user, String message) {
    emailClient.send(user.getEmail(), "您的订单已提交", message);
  }
  @Override
  protected String getChannelName() {
    return "email";
  }
}
public class SmsNotificationService extends NotificationService {
  private final SmsClient smsClient;
  public SmsNotificationService(SmsClient smsClient) {
    this.smsClient = smsClient;
  }
  @Override
  protected void validateContactMethod(User user) {
    if (user.getPhoneNumber() == null || user.getPhoneNumber().isBlank()) {
      throw new IllegalArgumentException("用户手机号不能为空");
    }
  }
  @Override
  protected void deliver(User user, String message) {
    smsClient.send(user.getPhoneNumber(), message);
  }
  @Override
  protected String getChannelName() {
    return "sms";
  }
}

Adding a new channel now requires implementing only three methods instead of copying 40 lines.

Refactoring Step 2: Introduce a NotificationMessage Value Object

The buildOrderMessage method returns a plain String, but different channels need different structures: email has a subject, SMS has only a body, push has both title and body. Returning a single string forces each channel to parse the body to extract a title.

A NotificationMessage record encapsulates title and body:

public record NotificationMessage(String title, String body) {
  public static NotificationMessage orderPlaced(Order order, User user) {
    String body = "您好 %s, 您的订单 #%s 已收到,总金额: %s".formatted(
      user.getName(), order.getId(), order.getTotalAmount());
    return new NotificationMessage("Order Confirmation", body);
  }
}

Updated Abstract Class

public abstract class NotificationService {
  private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
  public void sendOrderNotification(Order order, User user) {
    validateOrderAndUser(order, user);
    validateContactMethod(user);
    log.info("正在为用户 {} 发送订单 {} 的通知", user.getId(), order.getId());
    NotificationMessage message = NotificationMessage.orderPlaced(order, user);
    try {
      deliver(user, message);
      log.info("订单 {} 的{}发送成功", order.getId(), getChannelName());
    } catch (Exception e) {
      log.error("订单 {} 的{}发送失败", order.getId(), getChannelName(), e);
      throw new NotificationException(getChannelName() + "发送失败", e);
    }
  }
  // ... validateOrderAndUser, abstract methods unchanged
  protected abstract void deliver(User user, NotificationMessage message);
}

Now each channel receives a structured message and can use title, body, or both as appropriate.

Conclusion

By applying the DRY principle correctly — focusing on single source of knowledge rather than line-level duplication — the article shows how to eliminate 40 lines of repeated code across seven services using the Template Method pattern and a domain-specific value object. The result is a maintainable, extensible design where adding a new notification channel requires minimal, focused changes.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsJavaSpring BootRefactoringDRY PrincipleValue ObjectNotification ServiceTemplate Method Pattern
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.