A Spring Boot 3.x backend service with PostgreSQL and Redis, implementing the foundational entities and REST endpoints.
- Overview
- Tech Stack
- Project Structure
- Data Model
- API Reference
- Local Setup
- Seed Data
- Postman Collection
- Phase 2 Approach (Thread Safety)
- Phase 3 Approach (Notification Engine)
- Phase 4 Approach (Corner Cases)
Phase 1 establishes the core infrastructure:
- Spring Boot service wired to PostgreSQL via JPA/Hibernate
- Redis connection scaffolded and ready for Phase 2 guardrails
- Three REST endpoints for creating posts, adding comments, and liking posts
- Input validation with clean, structured error responses
- Auto-seeded actors (users + bot) for immediate testability
- Smart Phase 3 notification batching to avoid bot-notification spam
- Phase 4 hardening for race conditions, statelessness, and transaction integrity
| Layer | Technology |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.x |
| Database | PostgreSQL (via JPA / Hibernate) |
| Cache | Redis (scaffolded for Phase 2) |
| Build Tool | Maven |
| Dev Infra | Docker Compose |
project/
├── docker-compose.yml
├── pom.xml
├── postman/
│ └── Phase1.postman_collection.json
└── src/
└── main/
├── java/com/app/
│ ├── controller/ # REST controllers
│ ├── dto/ # Request / response DTOs
│ ├── entity/ # JPA entities
│ ├── repository/ # Spring Data JPA repositories
│ ├── service/ # Business logic
│ └── Application.java
└── resources/
└── application.yml
| Field | Type | Notes |
|---|---|---|
| id | Long | Primary key |
| username | String | Unique |
| is_premium | Boolean | Premium status flag |
| Field | Type | Notes |
|---|---|---|
| id | Long | Primary key |
| name | String | Bot identifier |
| persona_description | String | Bot persona/description |
| Field | Type | Notes |
|---|---|---|
| id | Long | Primary key |
| author_id | Long | FK → User or Bot |
| content | String | Post body |
| like_count | Integer | Tracks total likes |
| created_at | Timestamp | Auto-set on creation |
| Field | Type | Notes |
|---|---|---|
| id | Long | Primary key |
| post_id | Long | FK → Post |
| author_id | Long | FK → User or Bot |
| content | String | Comment body |
| depth_level | Integer | Nesting depth (1 = top-level) |
| created_at | Timestamp | Auto-set on creation |
Base URL: http://localhost:8080
Request body
{
"authorType": "USER",
"authorId": 1,
"content": "Launching the first draft of my post"
}| Field | Type | Required | Values |
|---|---|---|---|
| authorType | String | ✅ | USER, BOT |
| authorId | Long | ✅ | Existing ID |
| content | String | ✅ | Non-empty |
Request body
{
"authorType": "BOT",
"authorId": 3,
"content": "Nice post. I can help summarize the feedback.",
"depthLevel": 1
}| Field | Type | Required | Values |
|---|---|---|---|
| authorType | String | ✅ | USER, BOT |
| authorId | Long | ✅ | Existing ID |
| content | String | ✅ | Non-empty |
| depthLevel | Integer | ✅ | ≥ 1 |
Request body
{
"actorType": "USER",
"actorId": 2
}| Field | Type | Required | Values |
|---|---|---|---|
| actorType | String | ✅ | USER, BOT |
| actorId | Long | ✅ | Existing ID |
- Docker & Docker Compose
- Java 17+
- Maven 3.8+
docker compose up -dThis starts:
| Service | Host & Port | Credentials |
|---|---|---|
| PostgreSQL | localhost:5433 | db: phase1, user/pass: postgres |
| Redis | localhost:6379 | — |
mvn spring-boot:runThe app starts on http://localhost:8080.
On first boot, database tables are auto-created by Hibernate and seed data is inserted automatically.
The following actors are seeded on first run to make Phase 1 endpoints immediately testable:
| Type | ID | Identifier | Notes |
|---|---|---|---|
| User | 1 | alice |
is_premium=false |
| User | 2 | bob |
is_premium=true |
| Bot | 3 | zen-bot |
— |
Note: IDs may differ if your database already contains data.
A ready-to-import Postman collection covering all Phase 1 endpoints is included at:
postman/Phase1.postman_collection.json
Import it via Postman → File → Import and point the base URL to http://localhost:8080.
For Phase 2, I treated Redis as the gatekeeper and PostgreSQL as the source of truth.
In simple terms, every risky bot interaction is checked in Redis first (atomic lock rules), and only then persisted in PostgreSQL. This keeps the API stateless and safe under concurrent requests.
- Horizontal cap (max 100 bot replies per post)
- I use Redis atomic
INCRonpost:{postId}:bot_count. - Every request gets a unique increment result, even under heavy concurrency.
- If the incremented value is greater than 100, I reject the request with
429 Too Many Requestsand immediatelyDECRthe counter. - Because
INCR/DECRare atomic in Redis, parallel requests cannot bypass this limit.
- Cooldown cap (same bot cannot target same human twice in 10 minutes)
- I use Redis
SETwithNX+ TTL 10 minutes (setIfAbsent(..., Duration.ofMinutes(10))) on keycooldown:bot_{id}:human_{id}. - This operation is atomic: only the first concurrent request can create the key.
- All other concurrent requests for the same bot-human pair fail instantly and return
429.
- Vertical cap (max thread depth 20)
- I validate
depthLevelbefore writing anything. - If
depthLevel > 20, the request is rejected with429.
- Bot guardrails are reserved before writing the comment.
- If comment save or scoring fails, reservation is rolled back (
bot_countdecremented and cooldown key removed if needed). - This rollback prevents stale lock state and keeps Redis counters accurate.
- I update
post:{postId}:virality_scorein Redis with atomic increments:- Bot reply:
+1 - Human like:
+20 - Human comment:
+50
- Bot reply:
This gives fast real-time scoring while preserving correctness under parallel traffic.
Phase 3 introduces a Redis-backed notification strategy that reduces spam while still keeping users informed.
When a bot interacts with a user's post, the system checks a cooldown key:
- Cooldown key:
user:{id}:notif_cooldown - If cooldown exists: append a message to
user:{id}:pending_notifs - If cooldown does not exist: log
Push Notification Sent to Userand set cooldown TTL to 15 minutes
This means users do not get flooded with repeated bot push notifications in a short time window.
A Spring @Scheduled job runs every 5 minutes and scans pending notification lists (user:*:pending_notifs).
For each user list, it:
- Pops all queued messages from Redis
- Counts how many interactions were buffered
- Logs a summarized message in the format:
Summarized Push Notification: Bot X and [N] others interacted with your posts.
This keeps the user experience calm and digestible while preserving interaction visibility.
Phase 4 focuses on proving backend correctness under stress and failure conditions.
- The horizontal cap remains Redis-atomic (
INCR/DECR) and blocks requests once the counter exceeds 100. - To avoid drift after restarts, the Redis bot-counter key is seeded from PostgreSQL bot-comment count when missing.
- Result: even under burst concurrency, accepted bot comments for one post are capped at 100.
- No in-memory state is used for counters, cooldowns, or notification queues.
- All guardrails and notification buffers are stored in Redis keys/lists.
- The application instances remain stateless and horizontally scalable.
- Redis guardrails are reserved before write attempts.
- Reservation rollback is now bound to transaction completion status, so failed transactions release Redis reservations safely.
- Redis side-effects (virality increments and notifications) run after successful DB commit to prevent phantom updates when commits fail.
Integration tests were added in src/test/java/com/sam/assigment/service/Phase4CornerCasesIT.java to validate:
- 200 concurrent bot comment attempts produce exactly 100 persisted bot comments
- Redis reservation rollback on failed bot comment validation
Run the Phase 4 integration suite explicitly with:
mvn -Dphase4.it=true -Dtest=Phase4CornerCasesIT testThe tests are opt-in so default local/CI runs are not blocked in environments without Docker.