J
Jyotimoy Kashyap Systems & Architecture
Low-Level Design 2026-07-20

Task Management System LLD

An in-depth Low-Level Design (LLD) of an enterprise Task Management System featuring custom LRU caching, the Repository pattern, and thread-safe DAO patterns.

#Strategy Pattern #Observer Pattern #Repository Pattern #Builder Pattern
View Source on GitHub ↗

Task Management System: Low-Level Design (LLD)

Java Gradle Architecture Design Patterns

A modular, production-grade Low-Level Design (LLD) of an in-memory Task Management System built in pure Java. This project models core enterprise patterns for managing task lifecycles, state transitions, and assignments, reinforced with an integrated, custom Least Recently Used (LRU) Cache backed by a Doubly Linked List and HashMap, and a Domain-Driven Design (DDD) Event-Driven Notification Subsystem based on the Observer Pattern.


📌 Table of Contents


🎯 What We Are Building

In enterprise software engineering, task and project tracking backends (such as Jira, Linear, or Asana) face several fundamental challenges:

  1. High Read-to-Write Ratio: Tasks are viewed and inspected far more frequently than they are updated.
  2. Strict Domain Invariants: Invalid dates, null assignees during transitions, and empty task payloads must be rejected at the domain boundary.
  3. Data Access Decoupling: Business logic must not be tightly coupled to underlying storage engines (in-memory maps, SQL databases, or distributed caches like Redis).

This project demonstrates how to solve these challenges using a multi-tiered, clean architecture:

  • Rich Aggregate Root (Task): Encapsulates business invariants and records domain events internally upon state transitions.
  • Service Layer Gate (TaskService): Orchestrates persistence via TaskRepository, accepts single-request DTOs, and flushes domain events.
  • Data Transfer Objects (UpdateTaskRequest): Prevents endpoint explosion by accepting composite updates from web clients atomically.
  • Generic Notification Engine (Event<T>, EventPublisher, Subscriber): Fully decoupled Observer pattern framework ready for any entity (Tasks, Stories, Spikes).
  • Unified Repository Layer (TaskRepository): Orchestrates reads and writes across both persistent storage and cache layers.
  • Custom In-Memory LRU Cache (InMemoryCache): Built from fundamental data structures (DoublyLinkedList and HashMap) supporting constant-time O(1)O(1) access and eviction.
  • Persistent In-Memory DAO (InMemoryTaskDao): Implements thread-safe singleton storage with double-checked locking.

🏗 System Architecture

The project enforces clean unidirectional flow between DTO presentation, service orchestration, persistence, and event notification:

flowchart TD
    Client["Web Client / Main Entrypoint"] -->|"UpdateTaskRequest (DTO)"| Service["TaskService<br/>Service Boundary & Gate"]

    subgraph Domain_Model ["Domain Model (DDD)"]
        Service -->|"1. Mutate State"| Task["Task (Aggregate Root)<br/>Records DomainEvents on change"]
    end

    subgraph Persistence_Layer ["Persistence Layer"]
        Service -->|"2. Persist Task"| Repository["TaskRepository<br/>Cache & Storage Orchestration"]
        Repository -->|"Read Hit / Put / Evict"| Cache["CacheDao<br/>InMemoryCache - LRU"]
        Repository -->|"Read Miss / Write / Delete"| DB["TaskDao<br/>InMemoryTaskDao - Storage"]
        Cache --> DLL["DoublyLinkedList<br/>O(1) Sentinel Recency"]
        Cache --> Map["HashMap of UUID, Node<br/>O(1) Direct Lookup"]
    end

    subgraph Notification_Subsystem ["Notification Subsystem (Observer & Pluggable Executors)"]
        Service -->|"3. Pull & Publish Events"| Publisher["EventPublisher<br/>DefaultEventPublisher (Virtual Threads)"]
        Publisher -->|"4. Async Broadcast (Fire-and-Forget)"| Sub1["Subscriber<br/>EmailNotifier"]
        Publisher -->|"4. Async Broadcast (Fire-and-Forget)"| Sub2["Subscriber<br/>AuditNotifier / Webhook"]
    end

Layer Responsibilities

LayerComponentResponsibility
DTO LayerUpdateTaskRequestEncapsulates optional fields submitted by client in a single atomic payload.
Service LayerTaskServiceActs as the gate. Fetches aggregates, delegates domain operations, persists, and flushes events.
Domain LayerTask, User, StatusAggregate Root enforcing invariants and generating domain events on state change.
Notification LayerEvent<T>, EventPublisher, DefaultEventPublisher, SubscriberGeneric, domain-agnostic asynchronous pub-sub engine powered by CopyOnWriteArrayList and Java 21 Virtual Threads.
Repository LayerTaskRepositoryMediates between cache and database (Cache-Aside, Write-Through).
Cache LayerCacheDao / InMemoryCacheO(1)O(1) LRU cache maintaining active hot records.
Persistence LayerTaskDao / InMemoryTaskDaoThread-safe in-memory authoritative storage.
Data StructuresDoublyLinkedList, NodeCustom generic doubly linked list utilizing sentinel head/tail nodes.

📐 Class Blueprint & Domain Models

classDiagram
    %% ----------------- DOMAIN & DTO -----------------
    class Status {
        <<enumeration>>
        TODO
        IN_PROGRESS
        DONE
    }

    class User {
        -String username
        -UUID id
        +getUsername() String
        +getId() UUID
        +updateUserName(String)
        -validateUserName(String)
    }

    class Task {
        -UUID id
        -String name
        -String description
        -User assignedTo
        -LocalDate createdAt
        -LocalDate modifiedAt
        -LocalDate dueDate
        -Status status
        -List~Event~ domainEvents
        +getId() UUID
        +getName() String
        +getDescription() String
        +getAssignedTo() User
        +getDueDate() LocalDate
        +getStatus() Status
        +updateAssignee(User)
        +updateDueDate(LocalDate)
        +updateStatus(Status)
        +updateName(String)
        +updateDescription(String)
        +pullDomainEvents() List~Event~
    }

    class UpdateTaskRequest {
        <<record>>
        +UUID uuid
        +String name
        +String description
        +User assignedTo
        +LocalDate dueDate
        +Status status
    }

    %% ----------------- NOTIFICATION & OBSERVER -----------------
    class Event~T~ {
        <<interface>>
        +getEntityId() UUID
        +getEventName() String
        +getOldValue() T
        +getNewValue() T
        +getEventTime() Instant
    }

    class TaskAssigneeChangeEvent {
        -UUID entityId
        -User oldValue
        -User newValue
        -Instant time
        +getEntityId() UUID
        +getEventName() String
        +getOldValue() User
        +getNewValue() User
        +getEventTime() Instant
    }

    class EventPublisher {
        <<interface>>
        +addSubscriber(Subscriber)
        +removeSubscriber(Subscriber)
        +notify(Event~T~)
    }

    class SimpleEventPublisher {
        -List~Subscriber~ subscribers
        +addSubscriber(Subscriber)
        +removeSubscriber(Subscriber)
        +notify(Event~T~)
    }

    class DefaultEventPublisher {
        -CopyOnWriteArrayList~Subscriber~ subscribers
        -Executor executor
        -Logger LOGGER$
        +sync()$ DefaultEventPublisher
        +async()$ DefaultEventPublisher
        +addSubscriber(Subscriber)
        +removeSubscriber(Subscriber)
        +notify(Event~T~)
    }

    class Subscriber {
        <<interface>>
        +consume(Event~T~)
    }

    class EmailNotifier {
        +consume(Event~T~)
    }

    %% ----------------- SERVICE & REPOSITORY -----------------
    class TaskService {
        -TaskRepository repository
        -EventPublisher publisher
        +getTask(UUID) Task
        +getAllTasks() List~Task~
        +saveTask(Task) UUID
        +updateTask(UpdateTaskRequest)
        +updateTask(Task)
        +deleteTask(UUID)
    }

    class TaskRepository {
        -CacheDao cacheDao
        -TaskDao taskDao
        +getTask(UUID) Task
        +getAllTask() List~Task~
        +saveTask(Task)
        +updateTask(Task)
        +deleteTask(UUID)
    }

    %% ----------------- STORAGE & CACHE -----------------
    class TaskDao {
        <<interface>>
        +getTask(UUID) Optional~Task~
        +getAllTask() List~Task~
        +saveTask(Task) Optional~UUID~
        +updateTask(Task)
        +deleteTask(UUID)
    }

    class InMemoryTaskDao {
        -volatile InMemoryTaskDao INSTANCE$
        -Map~UUID, Task~ taskMap
        +getInstance()$ InMemoryTaskDao
    }

    class CacheDao {
        <<interface>>
        +get(UUID) Optional~Task~
        +put(Task)
        +remove(UUID)
    }

    class InMemoryCache {
        -Map~UUID, Node~Task~~ taskMap
        -DoublyLinkedList linkedList
        -int capacity
        +get(UUID) Optional~Task~
        +put(Task)
        +remove(UUID)
    }

    class DoublyLinkedList~V, N~ {
        ~N head
        ~N tail
        ~int size
        +addToHead(Node~V~)
        +remove(Node~V~)
        +removeTail() Optional~UUID~
    }

    %% Relationships
    Task --> Status : has
    Task --> User : assignedTo
    Task o-- Event : records
    Event <|.. TaskAssigneeChangeEvent : implements
    EventPublisher <|.. SimpleEventPublisher : implements
    EventPublisher <|.. DefaultEventPublisher : implements
    Subscriber <|.. EmailNotifier : implements
    SimpleEventPublisher o-- Subscriber : notifies
    DefaultEventPublisher o-- Subscriber : dispatches via Executor
    TaskService --> TaskRepository : persists via
    TaskService --> EventPublisher : flushes events to
    TaskService ..> UpdateTaskRequest : accepts
    TaskRepository --> CacheDao : caches in
    TaskRepository --> TaskDao : queries
    TaskDao <|.. InMemoryTaskDao : implements
    CacheDao <|.. InMemoryCache : implements
    InMemoryCache --> DoublyLinkedList : tracks recency

💡 Design Patterns Applied

1. Domain-Driven Design: Aggregate Root & Domain Events

  • Problem: Service-layer diffing (if (!existing.getAssignee().equals(incoming.getAssignee()))) is error-prone, violates DRY, and clutters orchestration code with manual property comparisons.
  • Solution: Task acts as an Aggregate Root (similar to Spring Data’s AbstractAggregateRoot). When state-changing methods (updateAssignee, updateStatus, updateDueDate) are invoked, Task encapsulates its own business invariants and appends an immutable domain event to an internal buffer. TaskService simply pulls uncommitted events (task.pullDomainEvents()) and dispatches them after persisting.
// Inside Task (Aggregate Root):
public void updateAssignee(User user) {
    if (Objects.equals(this.assignedTo, user)) return; // Guard against duplicate no-op
    User oldAssignee = this.assignedTo;
    this.assignedTo = user;
    this.domainEvents.add(new TaskAssigneeChangeEvent(this.id, oldAssignee, user));
    updateLastModifiedDate();
}

2. Observer Pattern: Generic Event Publisher, Subscribers & Pluggable Executors

  • Problem: Synchronous notification iteration (subscribers.forEach(...)) blocks the caller thread during slow subscriber I/O (e.g., SMTP in EmailNotifier), risks ConcurrentModificationException if subscribers are added/removed dynamically, and allows one failing subscriber to crash the entire update transaction.
  • Solution: DefaultEventPublisher implements the Observer pattern using Composition with a pluggable Executor strategy and CopyOnWriteArrayList:
    • Pluggable Dispatch (sync() vs async()): DefaultEventPublisher.async() uses Java 21 Virtual Threads (Executors.newVirtualThreadPerTaskExecutor()) for zero-overhead, fire-and-forget background delivery. DefaultEventPublisher.sync() passes Runnable::run for deterministic synchronous tests.
    • Thread-Safe, Lock-Free Iteration: Backed by CopyOnWriteArrayList<Subscriber>, iteration operates on an immutable snapshot, ensuring thread safety without locking.
    • Atomic Deduplication (addIfAbsent): Prevents “check-then-act” race conditions under concurrent registrations. Re-registering an existing subscriber logs a warning without throwing exceptions or halting the application.
    • Per-Subscriber Failure Isolation: Each subscriber executes inside its own executor task wrapped in a try-catch block so unexpected errors (e.g. webhook timeouts) never cascade.
// Asynchronous Fire-and-Forget Publisher (Java 21 Virtual Threads):
EventPublisher publisher = DefaultEventPublisher.async();

// Atomic deduplication & non-crashing warning log:
publisher.addSubscriber(emailNotifier);
publisher.addSubscriber(emailNotifier); // Logs: WARNING: Subscriber is already present

// In DefaultEventPublisher.java:
@Override
public <T> void notify(Event<T> event) {
    Objects.requireNonNull(event, "Event cannot be null");
    subscribers.forEach(subscriber -> {
        executor.execute(() -> {
            try {
                subscriber.consume(event);
            } catch (Exception e) {
                System.err.println("Failed to notify subscriber: " + e.getMessage());
            }
        });
    });
}

3. Data Transfer Object (DTO) Pattern: The Web Client Gate

  • Problem: Over HTTP/REST, a web client cannot mutate in-memory Java objects. Forcing individual endpoints for every single field (PATCH /assignee, PATCH /status) produces network chatty-ness and partial failure risks.
  • Solution: UpdateTaskRequest is an immutable record that encapsulates all optional fields submitted in a single request. TaskService acts as the gate: it loads the aggregate, executes only the relevant domain mutations, saves once, and flushes all resulting events atomically.
public void updateTask(UpdateTaskRequest request) {
    Task task = repository.getTask(request.uuid());

    if (request.name() != null) task.updateName(request.name());
    if (request.description() != null) task.updateDescription(request.description());
    if (request.assignedTo() != null) task.updateAssignee(request.assignedTo());
    if (request.dueDate() != null) task.updateDueDate(request.dueDate());
    if (request.status() != null) task.updateStatus(request.status());

    repository.updateTask(task);
    task.pullDomainEvents().forEach(publisher::notify);
}

4. Repository Pattern

  • Problem: When business services directly interact with both databases and caches, caching logic leaks throughout the service tier, producing code duplication and subtle bugs.
  • Solution: TaskRepository acts as an in-memory collection-like interface. TaskService does not know whether a task came from a fast memory cache or an underlying database; the repository seamlessly orchestrates lookups, saves, and updates.

5. Data Access Object (DAO) Pattern

  • Problem: Directly exposing storage implementations tightly binds domain rules to technical storage choices.
  • Solution: We define TaskDao and CacheDao interfaces. InMemoryTaskDao and InMemoryCache provide concrete implementations. If we decide to swap out InMemoryTaskDao for a relational SQL database via JDBC/Hibernate, or InMemoryCache for Redis, zero changes are required in TaskRepository or TaskService.

6. Singleton Pattern (Double-Checked Locking)

  • Problem: Multiple concurrent instances of an in-memory database DAO would fragment stored records into isolated states.
  • Solution: InMemoryTaskDao implements the Thread-Safe Singleton Pattern using a volatile instance reference and Double-Checked Locking. This avoids continuous synchronization bottlenecks while guaranteeing single-instance initialization.
public class InMemoryTaskDao implements TaskDao {
    private static volatile InMemoryTaskDao INSTANCE;
    private final Map<UUID, Task> taskMap;

    private InMemoryTaskDao() {
        taskMap = new HashMap<>();
    }

    public static InMemoryTaskDao getInstance() {
        if (INSTANCE == null) {
            synchronized (InMemoryTaskDao.class) {
                if (INSTANCE == null) {
                    INSTANCE = new InMemoryTaskDao();
                }
            }
        }
        return INSTANCE;
    }
}

7. Builder Pattern

  • Problem: Initializing a cache with multiple optional tuning parameters (initial capacity, eviction boundaries, load factors) via telescoping constructors is error-prone.
  • Solution: InMemoryCache.Builder provides a readable, fluent configuration interface while enforcing validation invariants (e.g., verifying capacity >0> 0) prior to constructing the immutable cache instance.
CacheDao cacheDao = new InMemoryCache.Builder()
        .capacity(2)
        .build();

8. Cache-Aside & Write-Through Caching

The repository employs a coordinated dual-datasource strategy:

Read (Cache-Aside with Lazy Loading):

  1. Query CacheDao.
  2. Cache Hit: Return item immediately. The underlying LRU cache promotes the item to the head of its recency list.
  3. Cache Miss: Retrieve the item from TaskDao (database).
  4. Store the fetched item into CacheDao for subsequent requests.
  5. Return the item.

Write / Update (Write-Through):

  1. Update TaskDao.
  2. Simultaneously update/refresh CacheDao (cacheDao.put(task)), ensuring immediate read-your-own-writes consistency.

Delete (Cache Invalidation):

  1. Remove from CacheDao.
  2. Remove from TaskDao.

9. O(1) LRU Eviction via Doubly Linked List

The InMemoryCache achieves constant time O(1)O(1) lookup, insertion, update, and eviction by combining:

  1. HashMap<UUID, Node<Task>>: Provides O(1)O(1) random access directly to any node in the list.
  2. DoublyLinkedList<Task, Node<Task>>: Maintains access order without requiring array shifts.
    • Uses dummy sentinel nodes (head and tail) to eliminate null pointer checks at list boundaries.
    • On Access (get): The accessed node is decoupled from its current position in O(1)O(1) and spliced right after head.
    • On Overflow (put when size >= capacity): The least recently used node immediately before tail (tail.prev) is evicted from both the list and the hash map in O(1)O(1).
[ Head Sentinel ] <---> [ Most Recent ] <---> ... <---> [ Least Recent ] <---> [ Tail Sentinel ]
                                                                 ^
                                                       (Evicted on Overflow)

🔔 Notification Subsystem & Event Architecture

The notification system models an enterprise Event-Driven Observer Pattern designed around domain lifecycle state transitions:

State Changes Tracked

  1. Assignee Change (TaskAssigneeChangeEvent): Emitted when a task is assigned, reassigned, or unassigned.
  2. Due Date Change: Notifies stakeholders of deadline postponements or escalations.
  3. Status Change: Signals state movement across TODO -> IN_PROGRESS -> DONE.

Event Dispatch & Notification Workflow

sequenceDiagram
    autonumber
    actor Client as "Web Client / Main"
    participant Service as "TaskService (The Gate)"
    participant Repo as TaskRepository
    participant Task as "Task (Aggregate Root)"
    participant Pub as "DefaultEventPublisher"
    participant Sub1 as "EmailNotifier (Virtual Thread 1)"
    participant Sub2 as "AuditNotifier (Virtual Thread 2)"

    Note over Client,Sub2: 1. Client Submits Update Request DTO
    Client->>Service: updateTask(UpdateTaskRequest)
    Service->>Repo: getTask(uuid)
    Repo-->>Service: Task (from Cache/DB)

    Note over Service,Task: 2. Domain Model Executes & Records Events
    Service->>Task: task.updateAssignee(alice)
    Task->>Task: Check change != current
    Task->>Task: Append TaskAssigneeChangeEvent to domainEvents

    Note over Service,Repo: 3. Persistence
    Service->>Repo: updateTask(task)
    Repo->>Repo: Write to DB & refresh Cache

    Note over Service,Pub: 4. Flush Uncommitted Events to Publisher
    Service->>Task: task.pullDomainEvents()
    Task-->>Service: [TaskAssigneeChangeEvent]
    Service->>Pub: notify(event)
    Pub-->>Service: Return immediately (Non-Blocking Fire-and-Forget)
    Service-->>Client: Update completed

    Note over Pub,Sub2: 5. Background Virtual Thread Dispatch (Parallel & Isolated)
    par Virtual Thread 1
        Pub-)Sub1: consume(event)
        Sub1->>Sub1: Dispatch email notification
    and Virtual Thread 2
        Pub-)Sub2: consume(event)
        Sub2->>Sub2: Record audit log
    end

Architectural Guarantees:

  • Non-Blocking Fire-and-Forget: Dispatching occurs on Java 21 Virtual Threads (Executors.newVirtualThreadPerTaskExecutor()), ensuring slow subscriber I/O never inflates client request latency.
  • Per-Subscriber Failure Isolation: Each subscriber runs in its own task shielded with try-catch. An unhandled exception (e.g. network failure) in one subscriber cannot prevent other subscribers from executing.
  • Thread-Safe Atomic Deduplication: CopyOnWriteArrayList.addIfAbsent() eliminates “check-then-act” race conditions under concurrent registrations; duplicates log a warning without crashing.
  • Zero Service-Side Diffing: The entity tracks what changed when it changed, capturing oldValue and newValue automatically.
  • Generic & Domain-Agnostic Engine: Event<T>, EventPublisher, and Subscriber do not depend on Task. They work for any entity (e.g., Stories, Spikes).
  • Atomic Single-Request Updates: Web clients submit one DTO payload; TaskService processes all mutations in one unit of work.

🔄 Core Workflows & Sequence Diagrams

1. Task Retrieval (Cache Miss -> DB Fetch -> Cache Backfill)

sequenceDiagram
    autonumber
    actor Client
    participant Service as TaskService
    participant Repo as TaskRepository
    participant Cache as InMemoryCache
    participant DB as InMemoryTaskDao

    Client->>Service: getTask(uuid)
    Service->>Repo: getTask(uuid)
    Repo->>Cache: get(uuid)
    
    alt Cache Hit
        Cache-->>Repo: Optional.of(task) (Promoted to Head)
        Repo-->>Service: Task
        Service-->>Client: Task
    else Cache Miss
        Cache-->>Repo: Optional.empty()
        Repo->>DB: getTask(uuid)
        DB-->>Repo: Task
        Repo->>Cache: put(task)
        Repo-->>Service: Task
        Service-->>Client: Task
    end

2. LRU Eviction on Capacity Exceeded

sequenceDiagram
    autonumber
    participant Repo as TaskRepository
    participant Cache as InMemoryCache
    participant DLL as DoublyLinkedList
    participant Map as taskMap

    Repo->>Cache: put(task3)
    Cache->>Cache: Check capacity (size >= capacity)
    Cache->>DLL: removeTail()
    DLL->>DLL: Decouple tail.prev
    DLL-->>Cache: Optional.of(evictedUUID)
    Cache->>Map: remove(evictedUUID)
    Cache->>DLL: addToHead(newNode)
    Cache->>Map: put(task3.getId(), newNode)
    Cache-->>Repo: void

📊 Complexity Analysis

OperationTime ComplexitySpace ComplexityDescription
getTask(id) (Cache Hit)O(1)O(1)O(1)O(1)Hash lookup + DLL pointer manipulation
getTask(id) (Cache Miss)O(1)O(1)O(1)O(1)DB hash lookup + Cache insertion
saveTask(task)O(1)O(1)O(1)O(1)Validation + DB insert + Cache insert
updateTask(task)O(1)O(1)O(1)O(1)DB replace + Cache promote/insert
deleteTask(id)O(1)O(1)O(1)O(1)DB remove + Cache node detachment
getAllTasks()O(N)O(N)O(N)O(N)Iterating over all active tasks in DB

Where CC is cache capacity and NN is total number of tasks in the database.


📁 Project Structure

task-management-system/
├── build.gradle                                     # Gradle build script
├── settings.gradle                                  # Gradle project settings
├── gradlew / gradlew.bat                            # Gradle wrapper scripts
├── README.md                                        # Architecture & design documentation
└── src/
    └── main/
        └── java/
            └── com/
                └── jyotimoykashyap/
                    ├── Main.java                    # Interactive lifecycle & notification demo
                    ├── models/
                    │   ├── Status.java              # Task status enum (TODO, IN_PROGRESS, DONE)
                    │   ├── User.java                # User domain model with validations & ID
                    │   └── Task.java                # Aggregate Root with domain event buffer
                    ├── dto/
                    │   └── UpdateTaskRequest.java   # Immutable DTO for atomic client updates
                    ├── datastructures/
                    │   ├── Node.java                # Generic Doubly-Linked List Node
                    │   └── DoublyLinkedList.java    # Custom O(1) Doubly Linked List with Sentinels
                    ├── cache/
                    │   ├── CacheDao.java            # Cache contract interface
                    │   └── InMemoryCache.java       # LRU Cache implementation with Builder
                    ├── dao/
                    │   ├── TaskDao.java             # Task persistence contract interface
                    │   └── InMemoryTaskDao.java     # Singleton In-Memory Task Database
                    ├── repository/
                    │   └── TaskRepository.java      # Coordinates Cache & DAO access
                    ├── service/
                    │   └── TaskService.java         # Public service gate & event dispatcher
                    └── notification/
                        ├── event/
                        │   ├── Event.java           # Generic event interface (entityId, eventName)
                        │   └── TaskAssigneeChangeEvent.java # Strongly-typed assignee change event
                        ├── publisher/
                        │   ├── DefaultEventPublisher.java # Asynchronous/Synchronous publisher with Virtual Threads
                        │   ├── EventPublisher.java  # Publisher contract
                        │   └── SimpleEventPublisher.java # Synchronous broadcast publisher
                        └── subscriber/
                            ├── Subscriber.java      # Generic subscriber consumer contract
                            └── EmailNotifier.java   # Concrete email subscriber

🚀 Running the Application

1. Prerequisites

  • JDK 21 or higher installed.

2. Build the Project

./gradlew build

3. Run the Demonstration

The included Main.java demonstrates LRU cache eviction (capacity = 2), atomic subscriber deduplication, non-blocking fire-and-forget Virtual Threads, and failure isolation:

# Compile and run via java directly:
./gradlew compileJava
java -cp build/classes/java/main com.jyotimoykashyap.Main

Expected Output

=== Starting Task Management System Demo ===

--- 0. Testing Subscriber Registration & Deduplication ---
Registered emailNotifier (First attempt: Success)
Attempting duplicate registration of the same emailNotifier:
WARNING: Subscriber is already present
Registered AuditNotifier & FaultyNotifier (for failure isolation testing).

--- 1. Creating and Saving Tasks ---
Saved Task 1: ffd2ba5b-49b8-49b2-a5df-eed5c18c4a1f
Saved Task 2: 71107acb-5e83-4496-a9a8-b35e1a329bd2

--- 2. Fetching Task 1 (Cache Hit & LRU Promotion) ---
Successfully fetched Task 1: ffd2ba5b-49b8-49b2-a5df-eed5c18c4a1f

--- 3. Saving Task 3 (Triggers LRU Eviction of Task 2) ---
Saved Task 3: baad6220-ad8f-4cc1-8acb-9e8473df37c7

--- 4. Fetching Task 2 (Cache Miss -> DB Fetch) ---
Successfully fetched Task 2 from DB: 71107acb-5e83-4496-a9a8-b35e1a329bd2

--- 5. Updating Task 1 (Assignee Change -> Async Notification) ---
Main thread: taskService.updateTask() completed immediately (non-blocking)!
⚠️  [FAULTY SUBSCRIBER] [Thread: VirtualThread[#31]/runnable@ForkJoinPool-1-worker-3] Simulating network failure...
Failed to notify subscriber: Simulated connection timeout to Slack Webhook
🔔 [AUDIT SUBSCRIBER] [Thread: VirtualThread[#30]/runnable@ForkJoinPool-1-worker-2 | Virtual: true] Event: TASK_ASSIGNEE_CHANGED
📧 [EMAIL NOTIFICATION] Event: TASK_ASSIGNEE_CHANGED | Entity ID: ffd2ba5b-49b8-49b2-a5df-eed5c18c4a1f | Old: None -> New: alice | At: 2026-09-14T10:11:36.790289Z

--- 6. Listing All Tasks ---
Total tasks in system: 3
 - Task ID: baad6220-ad8f-4cc1-8acb-9e8473df37c7
 - Task ID: 71107acb-5e83-4496-a9a8-b35e1a329bd2
 - Task ID: ffd2ba5b-49b8-49b2-a5df-eed5c18c4a1f

--- 7. Deleting Task 3 ---
Deleted Task 3.
Verified: Task 3 no longer exists (Task not found)

=== All Tests Completed Successfully! ===

🔮 Concurrency & Future Roadmap

While the current database DAO leverages thread-safe Singleton instantiation and maps, under heavy multi-threaded workloads the following enhancements can be incorporated:

  1. Concurrent Data Structures: Replacing raw HashMap with ConcurrentHashMap and wrapping doubly linked list operations in a ReentrantReadWriteLock.
  2. Asynchronous Dispatching: ✅ Completed via DefaultEventPublisher utilizing Java 21 Virtual Threads (Executors.newVirtualThreadPerTaskExecutor()) and CopyOnWriteArrayList with atomic addIfAbsent deduplication and failure isolation.
  3. Pluggable Eviction Strategies: Refactoring InMemoryCache to support pluggable policies (LFU, FIFO, Clock-Pro) via the Strategy Pattern (similar to cache-lld).