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
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)