DEV Community

Cover image for Reducing Duplication in Email Notification Settings with an Abstract Service in Spring Boot - Finovara
Marcin Parśniak
Marcin Parśniak

Posted on

Reducing Duplication in Email Notification Settings with an Abstract Service in Spring Boot - Finovara

When working on user notification settings in my Spring Boot project, I noticed a lot of duplicated logic across different email notification services

Each type of notification (password change, etc.) was doing almost the same thing:

  • loading the user
  • reading notification settings
  • updating a boolean flag
  • sending an email conditionally
  • logging activity

So I decided to refactor it using an abstract base service.

Before refactor (problem)

Every service contained similar logic like this:

User user = userManagerService.getUserByEmailOrThrow(email);
NotificationEmailSettings settings = user.getNotificationEmailSettings();

settings.setNotifyOnPasswordChange(dto.enabled());
Enter fullscreen mode Exit fullscreen mode

And then duplicated email sending + activity logic across multiple services.

This led to:

  1. duplicated code
  2. inconsistent behavior risk
  3. harder onboarding for new notification type

Solution: AbstractNotificationEmailService

I introduced a template-based abstract service that handles the shared workflow.

Now the base class looks like this:

@RequiredArgsConstructor
public abstract class AbstractNotificationEmailService {

    protected final UserManagerService userManagerService;
    protected final NotificationEmailSender notificationEmailSender;

    @Transactional
    public void saveEmailNotification(String email, NotificationEmailDto dto) {
        User user = userManagerService.getUserByEmailOrThrow(email);
        NotificationEmailSettings settings = user.getNotificationEmailSettings();

        boolean enabled = isEnabled(dto);
        applySetting(settings, enabled);

        handleActivity(email, enabled);
    }

    public NotificationEmailDto getEmailNotification(String email) {
        User user = userManagerService.getUserByEmailOrThrow(email);
        NotificationEmailSettings settings = user.getNotificationEmailSettings();

        return mapToDto(settings);
    }

    public void sendEmail(User user) {
        notificationEmailSender.sendIfEnabled(
                user,
                this::isNotificationEmailSettingsEnabled,
                this::sendEmailToUser
        );
    }

    protected abstract boolean isEnabled(NotificationEmailDto dto);

    protected abstract void applySetting(NotificationEmailSettings settings, boolean value);

    protected abstract boolean isNotificationEmailSettingsEnabled(NotificationEmailSettings settings);

    protected abstract NotificationEmailDto mapToDto(NotificationEmailSettings settings);

    protected abstract void sendEmailToUser(User user);

    protected void handleActivity(String email, boolean enabled) {
        // optional override
    }
}
Enter fullscreen mode Exit fullscreen mode

Example implementation

Each notification type now only defines its own behavior.

Example: password change notification

@Service
public class NotifyPasswordChangeService extends AbstractNotificationEmailService {

    private final PasswordChangeEmailService passwordChangeEmailService;
    private final SettingsActivityService settingsActivityService;

    public NotifyPasswordChangeService(
            UserManagerService userManagerService,
            NotificationEmailSender notificationEmailSender,
            PasswordChangeEmailService passwordChangeEmailService,
            SettingsActivityService settingsActivityService
    ) {
        super(userManagerService, notificationEmailSender);
        this.passwordChangeEmailService = passwordChangeEmailService;
        this.settingsActivityService = settingsActivityService;
    }

    @Override
    protected boolean isEnabled(NotificationEmailDto dto) {
        return dto.enabled();
    }

    @Override
    protected void applySetting(NotificationEmailSettings settings, boolean value) {
        settings.setNotifyOnPasswordChange(value);
    }

    @Override
    protected boolean isNotificationEmailSettingsEnabled(NotificationEmailSettings settings) {
        return settings.isNotifyOnPasswordChange();
    }

    @Override
    protected NotificationEmailDto mapToDto(NotificationEmailSettings settings) {
        return new NotificationEmailDto(settings.isNotifyOnPasswordChange());
    }

    @Override
    protected void sendEmailToUser(User user) {
        passwordChangeEmailService.sendEmail(user);
    }

    @Override
    protected void handleActivity(String email, boolean enabled) {
        settingsActivityService.createSettingActivity(
                email,
                enabled ? SettingActivityStatus.ENABLED : SettingActivityStatus.DISABLED,
                SettingType.NOTIFICATION_PASSWORD_CHANGED
        );
    }
}

Enter fullscreen mode Exit fullscreen mode

What improved

After this refactor:

  • Better structure: shared logic is in one place
  • Less duplication: no repeated boilerplate across services
  • Easier extension:new notification = just extend abstract class
  • More consistency: same flow for every notification type

Thanks for reading, if you want check my github!

GitHub logo M4rc1nek / finovara-backend

Backend service for a personal finance management application

Finovara

Finovara is a financial management platform designed to help users effectively track analyze, and optimize their income, expenses, and savings The application provides a secure, bank-like experience focused on transparency, financial awareness, and long-term money planning.

🎯 Purpose of the Application

Finovara aims to support users in making better financial decisions by offering clear insights into their financial activity and helping them maintain control over their budgets and savings.

The platform focuses on:

  • organizing income and expenses in a structured way
  • visualizing financial data through charts and statistics
  • supporting saving goals and spending limits
  • providing a virtual wallet concept for daily financial management

🚀 Key Features

  • Secure user authentication and authorization
  • Income and expense tracking
  • Categorization of financial operations
  • Interactive charts and financial statistics
  • Reports summarizing spending and income trends
  • Virtual wallet management
  • Savings goals (e.g. piggy banks)
  • Spending limits and budget control
  • Scalable architecture prepared for future financial…

Top comments (0)