This project is a Spring Boot microservice that uses PostgreSQL as the source of truth for posts and comments, and Redis as the stateless guardrail layer for concurrency control, cooldowns, virality scoring, and notification batching.
- Java 17
- Spring Boot 3.3
- Spring Data JPA
- Spring Data Redis
- PostgreSQL 16
- Redis 7
- Docker Compose
- JUnit 5 / Mockito
POST /api/poststo create a postPOST /api/posts/{postId}/commentsto create a commentPOST /api/posts/{postId}/liketo like a post- Redis virality score updates in real time
- Horizontal bot reply cap of
100per post - Vertical thread depth cap of
20 - Bot-to-human cooldown of
10minutes - Notification throttling with pending Redis list batching
- Scheduled sweeper every
5minutes
User:id,username,is_premiumBot:id,name,persona_descriptionPost:id,author_id,author_type,content,created_atComment:id,post_id,parent_comment_id,author_id,author_type,content,depth_level,created_at
author_type was added alongside author_id so the service can safely distinguish whether an actor is a human or a bot.
Redis key: post:{id}:virality_score
- Bot reply:
+1 - Human like:
+20 - Human comment:
+50
Redis key: post:{id}:bot_count
- Every bot reply first reserves a slot using atomic Redis
INCR - If the incremented value is greater than
100, the request is rejected with429 Too Many Requests - The slot is immediately released with
DECR - This ensures that even under concurrent request races, only the first
100bot replies are allowed through
- Redis guardrails are checked before the database write is considered successful
- If a database transaction fails after a bot slot is reserved, the reserved slot is released during rollback handling
- This prevents Redis counters from drifting away from persisted database state
- Nested comments compute
depthLevel = parent.depthLevel + 1 - Any comment request producing
depthLevel > 20is rejected
Redis key: cooldown:bot_{botId}:human_{humanId}
- TTL:
10minutes - If the key already exists, the bot interaction is rejected with
429 Too Many Requests
When a bot interacts with a user's post:
- If the user has not received a notification in the last
15minutes, the app logs:Push Notification Sent to User
- A notification cooldown key is then set in Redis
- If the user is still inside the cooldown window, the message is pushed into:
user:{id}:pending_notifs
- The user id is also registered in:
pending_notif_users
The scheduled sweeper runs every 5 minutes and:
- scans all users with pending notifications
- drains their Redis list
- logs a summarized message such as:
Summarized Push Notification: Bot X and [N] others interacted with your posts.
docker compose up -dmvn spring-boot:runmvn test- PostgreSQL URL:
jdbc:postgresql://localhost:5432/assignment_db - PostgreSQL username:
postgres - PostgreSQL password:
postgres - Redis host:
localhost - Redis port:
6379
Supported environment overrides:
DB_URLDB_USERNAMEDB_PASSWORDREDIS_HOSTREDIS_PORT
The app starts with sample seed data from src/main/resources/data.sql:
- Users:
1 -> alice2 -> bob
- Bots:
1 -> Nova2 -> Atlas
This makes the Postman collection usable immediately after startup.
POST /api/posts
{
"authorType": "USER",
"authorId": 1,
"content": "Hello world"
}POST /api/posts/{postId}/comments
{
"authorType": "BOT",
"authorId": 1,
"parentCommentId": null,
"content": "Automated reply"
}POST /api/posts/{postId}/like
{
"userId": 1
}Automated tests cover:
- the
200concurrent bot request spam case, asserting exactly100successful bot replies - rejection when comment depth exceeds
20 - rejection when a bot hits the same human again within the
10minute cooldown - notification batching behavior while the user notification cooldown is active
- The repository includes source code,
docker-compose.yml, README, and Postman collection - Redis is used for all counters, cooldowns, and pending notification batching
- PostgreSQL remains the source of truth for persisted content
- The application is stateless with no in-memory production counters or static state