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.
Task Management System: Low-Level Design (LLD)
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
- System Architecture
- Class Blueprint & Domain Models
- Design Patterns Applied
- 1. Domain-Driven Design: Aggregate Root & Domain Events
- 2. Observer Pattern: Generic Event Publisher & Subscribers
- 3. Data Transfer Object (DTO) Pattern: The Web Client Gate
- 4. Repository Pattern
- 5. Data Access Object (DAO) Pattern
- 6. Singleton Pattern (Double-Checked Locking)
- 7. Builder Pattern
- 8. Cache-Aside & Write-Through Caching
- 9. O(1) LRU Eviction via Doubly Linked List
- Notification Subsystem & Event Architecture
- Core Workflows & Sequence Diagrams
- Complexity Analysis
- Project Structure
- Running the Application
🎯 What We Are Building
In enterprise software engineering, task and project tracking backends (such as Jira, Linear, or Asana) face several fundamental challenges:
- High Read-to-Write Ratio: Tasks are viewed and inspected far more frequently than they are updated.
- Strict Domain Invariants: Invalid dates, null assignees during transitions, and empty task payloads must be rejected at the domain boundary.
- 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 viaTaskRepository, 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 (DoublyLinkedListandHashMap) supporting constant-time 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
| Layer | Component | Responsibility |
|---|---|---|
| DTO Layer | UpdateTaskRequest | Encapsulates optional fields submitted by client in a single atomic payload. |
| Service Layer | TaskService | Acts as the gate. Fetches aggregates, delegates domain operations, persists, and flushes events. |
| Domain Layer | Task, User, Status | Aggregate Root enforcing invariants and generating domain events on state change. |
| Notification Layer | Event<T>, EventPublisher, DefaultEventPublisher, Subscriber | Generic, domain-agnostic asynchronous pub-sub engine powered by CopyOnWriteArrayList and Java 21 Virtual Threads. |
| Repository Layer | TaskRepository | Mediates between cache and database (Cache-Aside, Write-Through). |
| Cache Layer | CacheDao / InMemoryCache | LRU cache maintaining active hot records. |
| Persistence Layer | TaskDao / InMemoryTaskDao | Thread-safe in-memory authoritative storage. |
| Data Structures | DoublyLinkedList, Node | Custom 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:
Taskacts as an Aggregate Root (similar to Spring Data’sAbstractAggregateRoot). When state-changing methods (updateAssignee,updateStatus,updateDueDate) are invoked,Taskencapsulates its own business invariants and appends an immutable domain event to an internal buffer.TaskServicesimply 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 inEmailNotifier), risksConcurrentModificationExceptionif subscribers are added/removed dynamically, and allows one failing subscriber to crash the entire update transaction. - Solution:
DefaultEventPublisherimplements the Observer pattern using Composition with a pluggableExecutorstrategy andCopyOnWriteArrayList:- Pluggable Dispatch (
sync()vsasync()):DefaultEventPublisher.async()uses Java 21 Virtual Threads (Executors.newVirtualThreadPerTaskExecutor()) for zero-overhead, fire-and-forget background delivery.DefaultEventPublisher.sync()passesRunnable::runfor 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-catchblock so unexpected errors (e.g. webhook timeouts) never cascade.
- Pluggable Dispatch (
// 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:
UpdateTaskRequestis an immutable record that encapsulates all optional fields submitted in a single request.TaskServiceacts 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:
TaskRepositoryacts as an in-memory collection-like interface.TaskServicedoes 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
TaskDaoandCacheDaointerfaces.InMemoryTaskDaoandInMemoryCacheprovide concrete implementations. If we decide to swap outInMemoryTaskDaofor a relational SQL database via JDBC/Hibernate, orInMemoryCachefor Redis, zero changes are required inTaskRepositoryorTaskService.
6. Singleton Pattern (Double-Checked Locking)
- Problem: Multiple concurrent instances of an in-memory database DAO would fragment stored records into isolated states.
- Solution:
InMemoryTaskDaoimplements the Thread-Safe Singleton Pattern using avolatileinstance 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.Builderprovides a readable, fluent configuration interface while enforcing validation invariants (e.g., verifying capacity ) 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):
- Query
CacheDao. - Cache Hit: Return item immediately. The underlying LRU cache promotes the item to the head of its recency list.
- Cache Miss: Retrieve the item from
TaskDao(database). - Store the fetched item into
CacheDaofor subsequent requests. - Return the item.
Write / Update (Write-Through):
- Update
TaskDao. - Simultaneously update/refresh
CacheDao(cacheDao.put(task)), ensuring immediate read-your-own-writes consistency.
Delete (Cache Invalidation):
- Remove from
CacheDao. - Remove from
TaskDao.
9. O(1) LRU Eviction via Doubly Linked List
The InMemoryCache achieves constant time lookup, insertion, update, and eviction by combining:
HashMap<UUID, Node<Task>>: Provides random access directly to any node in the list.DoublyLinkedList<Task, Node<Task>>: Maintains access order without requiring array shifts.- Uses dummy sentinel nodes (
headandtail) to eliminate null pointer checks at list boundaries. - On Access (
get): The accessed node is decoupled from its current position in and spliced right afterhead. - On Overflow (
putwhensize >= capacity): The least recently used node immediately beforetail(tail.prev) is evicted from both the list and the hash map in .
- Uses dummy sentinel nodes (
[ 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
- Assignee Change (
TaskAssigneeChangeEvent): Emitted when a task is assigned, reassigned, or unassigned. - Due Date Change: Notifies stakeholders of deadline postponements or escalations.
- 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
oldValueandnewValueautomatically. - Generic & Domain-Agnostic Engine:
Event<T>,EventPublisher, andSubscriberdo not depend onTask. They work for any entity (e.g., Stories, Spikes). - Atomic Single-Request Updates: Web clients submit one DTO payload;
TaskServiceprocesses 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
| Operation | Time Complexity | Space Complexity | Description |
|---|---|---|---|
getTask(id) (Cache Hit) | Hash lookup + DLL pointer manipulation | ||
getTask(id) (Cache Miss) | DB hash lookup + Cache insertion | ||
saveTask(task) | Validation + DB insert + Cache insert | ||
updateTask(task) | DB replace + Cache promote/insert | ||
deleteTask(id) | DB remove + Cache node detachment | ||
getAllTasks() | Iterating over all active tasks in DB |
Where is cache capacity and 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:
- Concurrent Data Structures: Replacing raw
HashMapwithConcurrentHashMapand wrapping doubly linked list operations in aReentrantReadWriteLock. - Asynchronous Dispatching: ✅ Completed via
DefaultEventPublisherutilizing Java 21 Virtual Threads (Executors.newVirtualThreadPerTaskExecutor()) andCopyOnWriteArrayListwith atomicaddIfAbsentdeduplication and failure isolation. - Pluggable Eviction Strategies: Refactoring
InMemoryCacheto support pluggable policies (LFU, FIFO, Clock-Pro) via the Strategy Pattern (similar tocache-lld).