While working on my project Finovara, I recently implemented a flexible notification system that reacts to user actions — like reaching a savings goal in a piggy bank.
Instead of going with a rigid structure, I decided to build something extensible and future-proof.
🧠 The Problem
Users perform different actions in the system:
- reaching a savings goal
- completing financial milestones
- (more coming soon…)
Each of these events should generate a notification, but with different payloads.
I didn’t want:
- dozens of tables
- messy conditional logic
- tightly coupled notification handling
⚙️ The Approach
I used JSON polymorphism with Jackson to store notifications in a single table, while keeping type-specific structures.
Each notification implements a common interface:
public interface NotificationResponse {
NotificationType type();
LocalDate createdAt();
String deduplicationKey();
}
And then I define specific types like this:
@JsonTypeName("PIGGY_BANK_GOAL_REACHED")
public record PiggyBankReachedDto(
NotificationType type,
LocalDate createdAt,
PiggyBankGoalType goalType,
String piggyBankName,
Long piggyBankId,
BigDecimal threshold
) implements NotificationResponse {
@Override
public String deduplicationKey() {
return "%s:%d:%s:%s".formatted(type, piggyBankId, goalType, threshold);
}
}
🧩 Generic Notification Engine
To avoid duplicating logic, I introduced a generic abstraction:
public abstract class ThresholdReachedService<E, C> implements NotificationCreator {
protected static final BigDecimal REACHED_THRESHOLD = BigDecimal.valueOf(100);
protected abstract List<E> fetchEntities(Long userId);
protected abstract C calculate(E entity, Long userId);
protected abstract BigDecimal getPercentage(C context);
protected abstract NotificationResponse buildNotification(E entity, C context, Long userId);
@Override
public List<NotificationResponse> getNotifications(Long userId) {
List<NotificationResponse> result = new ArrayList<>();
for (E entity : fetchEntities(userId)) {
C context = calculate(entity, userId);
BigDecimal percentage = getPercentage(context);
if (percentage.compareTo(REACHED_THRESHOLD) >= 0) {
result.add(buildNotification(entity, context, userId));
}
}
return result;
}
}
This allows me to:
- fetch domain entities
- calculate progress
- generate notifications only when a threshold is reached
💾 Persistence with Deduplication
One tricky part: avoiding duplicate notifications.
I solved it with a deduplicationKey:
.filter(dto -> !existingKeys.contains(dto.deduplicationKey()))
.filter(dto -> batchKeys.add(dto.deduplicationKey()))
This ensures:
- no duplicates from previous runs
- no duplicates within the same batch
🔄 Serialization Layer
Notifications are stored as JSON:
payload(toJson(dto))
and safely deserialized:
objectMapper.readValue(payload, NotificationResponse.class);
This gives me:
- flexibility in adding new notification types
- no schema changes needed
- clean separation of concerns
✅ What I Gained
✔️ Extensible architecture
✔️ Clean domain separation
✔️ No schema migrations for new notification types
✔️ Easy to plug in new features
Thanks for reading, and check out 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 (0)