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());
And then duplicated email sending + activity logic across multiple services.
This led to:
- duplicated code
- inconsistent behavior risk
- 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
}
}
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
);
}
}
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!
M4rc1nek
/
finovara-backend
Backend service for a personal finance management application
💰 Finovara — Backend
Backend REST API for a personal finance management application built with Java 25 and Spring Boot 4.
📖 About the Project
Finovara is a personal finance platform designed to help users take full control of their money. The backend exposes a secure REST API that powers tracking of income and expenses, budget management, savings goals, and financial reporting — all wrapped in a bank-grade security model based on JWT authentication.
The application is designed with scalability in mind and is fully containerized via Docker, with separate production and test database environments managed through Docker Compose.
🎯 Key Features
- 🔐 Authentication & Authorization — JWT-based stateless security with Spring Security; access and refresh token flow with device/user-agent detection
- 💸 Income & Expense Tracking — full CRUD for financial operations with category tagging
- 📊 Statistics & Reports — aggregated financial summaries, spending trends, and exportable PDF reports
- 🏦…
Top comments (2)
This is such a clean use of abstraction
most people either copy paste this logic everywhere
or overcomplicate it with too many layers
template method here feels like the right balance
also like how adding a new notification is just
“extend + implement specifics”
makes scaling this way less painful later
thank you so much!