DEV Community

Cover image for How I Designed a Scalable Notification System with JSON Polymorphism in Spring Boot - Finovara
Marcin Parśniak
Marcin Parśniak

Posted on

How I Designed a Scalable Notification System with JSON Polymorphism in Spring Boot - Finovara

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

🧩 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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()))
Enter fullscreen mode Exit fullscreen mode

This ensures:

  • no duplicates from previous runs
  • no duplicates within the same batch

🔄 Serialization Layer

Notifications are stored as JSON:

payload(toJson(dto))
Enter fullscreen mode Exit fullscreen mode

and safely deserialized:

objectMapper.readValue(payload, NotificationResponse.class);
Enter fullscreen mode Exit fullscreen mode

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!

GitHub logo M4rc1nek / finovara-backend

Backend service for a personal finance management application

Finovara

Backend platform for a personal finance management application, built with Java 25 and Spring Boot 4 as a set of microservices.

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, shared accounts and financial reporting.

The platform is built as a set of independent services, each responsible for a separate area of the domain, so that individual parts of the system can be developed, deployed and scaled on their own. Security is a central concern: authentication is based on JWT, sessions are tracked per device, and user activity is recorded in an audit log.


Key Features

Accounts and Security

  • Authentication and Authorization - stateless JWT security built on Spring Security, with an access and refresh token flow.
  • Device and Session Tracking…




Top comments (0)