A real-time chat backend built with Spring Boot 3.5 and Redis — leveraging Pub/Sub for live message broadcasting, Redis Lists for persistent history, and a clean REST API testable via Postman.
- Overview
- Features
- Tech Stack
- Prerequisites
- Getting Started
- Running Tests
- API Documentation
- Project Structure
- Redis Data Model
- Real-Time Messaging Flow
- Error Handling
- Configuration
This is a backend-only REST API for a real-time chat application. It uses Redis as the sole data store, mapping every concept (rooms, participants, messages) to purpose-built Redis data structures. Real-time message broadcasting is powered by Redis Pub/Sub via a wildcard PatternTopic subscription (chatroom.topic.*), so a single subscriber handles all rooms.
There is no UI — the API is fully testable with Postman.
| Feature | Implementation Detail |
|---|---|
| 🏠 Chat Rooms | Created and stored as Redis Hashes (chatroom:{roomId}:meta) |
| 👥 Participants | Tracked per room in Redis Sets (chatroom:{roomId}:users) |
| 📜 Message History | Stored in Redis Lists (chatroom:{roomId}:messages) with configurable limit (default: 20) |
| ⚡ Real-Time Messaging | Published to chatroom.topic.{roomId} channel; received by RedisMessageSubscriber |
| 🔑 Unique Room IDs | roomId equals roomName — duplicate names throw a 400 Bad Request |
| 🗑️ Room Deletion | Atomically removes all 3 Redis keys for a room in a single DEL command |
| 🛡️ Error Handling | @RestControllerAdvice handles IllegalArgumentException (400) and generic exceptions (500) |
| 🧪 TDD | 9 passing tests covering service logic, Pub/Sub verification, and controller endpoints |
| Layer | Technology |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.5.10 |
| Data Store | Redis (via Lettuce client) |
| Redis Serialization | StringRedisSerializer (keys) + GenericJackson2JsonRedisSerializer (values) |
| Pub/Sub | RedisMessageListenerContainer + PatternTopic |
| Boilerplate Reduction | Lombok (@Data, @RequiredArgsConstructor) |
| DTOs | Java Records (CreateRoomRequest, JoinRequest, SendMessageRequest) |
| Build Tool | Maven 3.9.12 (Wrapper included) |
| Testing | JUnit 5 + Mockito (@WebMvcTest, @ExtendWith(MockitoExtension.class)) |
| IDE | IntelliJ IDEA |
| OS | Windows 11 (PowerShell) |
1. Java 17+
java -version2. Redis Server running on localhost:6379
Windows Users: Redis may be running as a background Windows Service even if
redis-cliisn't on your PATH. Verify from the install directory:cd "C:\Program Files\Redis" .\redis-cli.exe pingExpected output:
PONG
No authentication or non-default Redis configuration is needed — the app connects to localhost:6379 with no password.
git clone https://github.com/shendeyogesh11/chat-application.git
cd chat-applicationOpen PowerShell (or any terminal) in the project root — the directory containing pom.xml — and run:
.\mvnw spring-boot:runWait for the startup confirmation:
Started ChatappApplication in X.XXX seconds (JVM running for X.XXX)
The server is now live at http://localhost:8080.
Press Ctrl + C in the terminal. Type Y if prompted to terminate the batch job.
The project follows Test-Driven Development (TDD). The suite has 9 tests across three test classes:
| Test Class | Type | Tests Covered |
|---|---|---|
ChatServiceTest |
Unit (Mockito) | Create room success, duplicate room error, join room success, join non-existent room error, send message with Pub/Sub verification |
ChatControllerTest |
Slice (@WebMvcTest) |
Create room API, join room API, retrieve chat history API |
ChatappApplicationTests |
Integration | Spring context loads |
Run all tests with:
.\mvnw testExpected output:
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Note:
ChatappApplicationTests(context load) requires Redis to be running. All service and controller tests use Mockito mocks and do not require a live Redis instance.
Base URL: http://localhost:8080/api/chatapp/chatrooms
Creates a new room. The roomId returned is identical to the roomName provided.
POST /api/chatapp/chatrooms
Request Body:
{
"roomName": "general"
}Response 200 OK:
{
"message": "Chat room 'general' created successfully.",
"roomId": "general",
"status": "success"
}Error — Duplicate name 400 Bad Request:
{
"error": "Chat room already exists: general",
"status": "error"
}Adds a participant to an existing room's Redis Set. Joining is idempotent — adding the same participant twice has no side effects (Redis Set semantics).
POST /api/chatapp/chatrooms/{roomId}/join
Example: POST /api/chatapp/chatrooms/general/join
Request Body:
{
"participant": "guest_user"
}Response 200 OK:
{
"message": "User 'guest_user' joined chat room 'general'.",
"status": "success"
}Error — Room not found 400 Bad Request:
{
"error": "Room not found: general",
"status": "error"
}Sends a message to a room. The timestamp is set server-side using Instant.now() (ISO-8601). The message is both persisted to a Redis List and published to the room's Pub/Sub channel.
POST /api/chatapp/chatrooms/{roomId}/messages
Example: POST /api/chatapp/chatrooms/general/messages
Request Body:
{
"participant": "guest_user",
"message": "Hello, everyone!"
}Response 200 OK:
{
"message": "Message sent successfully.",
"status": "success"
}Real-Time Verification: After sending, check the application console for the Pub/Sub broadcast log:
[REAL-TIME EVENT]
Room: chatroom.topic.general
User: guest_user
Said: "Hello, everyone!"
Timestamp: 2024-01-01T10:00:00.000000000Z
Error — Room not found 400 Bad Request:
{
"error": "Room not found: general",
"status": "error"
}Returns the last N messages from a room in chronological order. Defaults to the last 20 messages if limit is omitted or <= 0.
GET /api/chatapp/chatrooms/{roomId}/messages?limit={n}
Example: GET /api/chatapp/chatrooms/general/messages?limit=10
Response 200 OK:
{
"messages": [
{
"participant": "guest_user",
"message": "Hello, everyone!",
"timestamp": "2024-01-01T10:00:00.000000000Z"
},
{
"participant": "another_user",
"message": "Hi, guest_user!",
"timestamp": "2024-01-01T10:01:00.000000000Z"
}
]
}Permanently removes all 3 Redis keys for a room (meta, users, messages) in a single atomic DEL command.
DELETE /api/chatapp/chatrooms/{roomId}
Example: DELETE /api/chatapp/chatrooms/general
Response 200 OK:
{
"message": "Chat room 'general' deleted successfully.",
"status": "success"
}| Method | Endpoint | Description |
|---|---|---|
POST |
/api/chatapp/chatrooms |
Create a chat room |
POST |
/api/chatapp/chatrooms/{roomId}/join |
Join a chat room |
POST |
/api/chatapp/chatrooms/{roomId}/messages |
Send a message |
GET |
/api/chatapp/chatrooms/{roomId}/messages?limit=N |
Retrieve chat history (default limit: 20) |
DELETE |
/api/chatapp/chatrooms/{roomId} |
Delete a chat room |
chat-application/
├── pom.xml
├── mvnw / mvnw.cmd # Maven wrapper scripts
└── src/
├── main/
│ ├── java/com/backend/assignment/chatapp/
│ │ ├── ChatappApplication.java # @SpringBootApplication entry point
│ │ ├── config/
│ │ │ └── RedisConfig.java # RedisTemplate (JSON serialization) +
│ │ │ # Pub/Sub container (PatternTopic: chatroom.topic.*)
│ │ ├── controller/
│ │ │ └── ChatController.java # @RestController — 5 REST endpoints
│ │ ├── dto/
│ │ │ ├── CreateRoomRequest.java # record(String roomName)
│ │ │ ├── JoinRequest.java # record(String participant)
│ │ │ └── SendMessageRequest.java # record(String participant, String message)
│ │ ├── exception/
│ │ │ └── GlobalExceptionHandler.java # @RestControllerAdvice — maps exceptions to HTTP status
│ │ ├── model/
│ │ │ ├── ChatMessage.java # @Data — participant, message, timestamp (ISO-8601)
│ │ │ └── ChatRoom.java # @Data — roomId, name
│ │ ├── repository/
│ │ │ └── ChatRepository.java # All Redis Hash / Set / List operations
│ │ └── service/
│ │ ├── ChatService.java # Business logic + Pub/Sub publish
│ │ └── RedisMessageSubscriber.java # MessageListener — deserializes & logs real-time events
│ └── resources/
│ └── application.properties # Redis host, port, Lettuce pool config
└── test/
└── java/com/backend/assignment/chatapp/
├── ChatappApplicationTests.java # Spring context load test
├── ChatControllerTest.java # @WebMvcTest — MockMvc endpoint assertions
└── ChatServiceTest.java # Mockito unit tests — business logic & Pub/Sub
Every operation maps to a specific Redis primitive. Keys are namespaced by {roomId}.
| Concept | Redis Type | Exact Key | Content |
|---|---|---|---|
| Room Metadata | Hash | chatroom:{roomId}:meta |
Fields: id → roomId, name → roomName |
| Participants | Set | chatroom:{roomId}:users |
Unique participant name strings |
| Messages | List | chatroom:{roomId}:messages |
JSON-serialized ChatMessage objects (appended via RPUSH) |
| Real-Time Events | Pub/Sub | chatroom.topic.{roomId} |
JSON-serialized ChatMessage broadcasts |
Room existence check: redisTemplate.hasKey("chatroom:{roomId}:meta")
Message format stored in Redis List:
{
"@class": "com.backend.assignment.chatapp.model.ChatMessage",
"participant": "guest_user",
"message": "Hello, everyone!",
"timestamp": "2024-01-01T10:00:00.000000000Z"
}Values are serialized using
GenericJackson2JsonRedisSerializer, which embeds type metadata (@class). Keys are plain strings viaStringRedisSerializerand are fully readable inredis-cli.
When POST /chatrooms/{roomId}/messages is called:
ChatController
│
▼
ChatService.sendMessage()
│
├── 1. Sets timestamp server-side: Instant.now().toString()
│
├── 2. chatRepository.saveMessage()
│ └── RPUSH chatroom:{roomId}:messages → Redis List (persistence)
│
└── 3. redisTemplate.convertAndSend("chatroom.topic.{roomId}", message)
└── Redis Pub/Sub publish
│
▼
RedisMessageListenerContainer
(PatternTopic: "chatroom.topic.*")
│
▼
RedisMessageSubscriber.onMessage()
└── Deserializes JSON → ChatMessage
└── Logs [REAL-TIME EVENT] to application console
In a production system,
RedisMessageSubscriber.onMessage()would push events to connected WebSocket clients or Server-Sent Events (SSE) streams instead of logging to the console.
All exceptions are caught by GlobalExceptionHandler (@RestControllerAdvice) and return a consistent JSON envelope.
| Scenario | Exception | HTTP Status | Example Response |
|---|---|---|---|
| Duplicate room name | IllegalArgumentException |
400 Bad Request |
{ "error": "Chat room already exists: general", "status": "error" } |
| Room not found (any operation) | IllegalArgumentException |
400 Bad Request |
{ "error": "Room not found: general", "status": "error" } |
| Unexpected server error | Exception |
500 Internal Server Error |
{ "error": "An unexpected error occurred.", "details": "..." } |
All business rule violations use
IllegalArgumentExceptionand map to400 Bad Request. The application does not use404 Not Foundin the current implementation.
src/main/resources/application.properties:
spring.application.name=chatapp
# Redis Connection
spring.data.redis.host=localhost
spring.data.redis.port=6379
# Lettuce Connection Pool (handles concurrent requests)
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.lettuce.pool.max-wait=-1msTo connect to a remote Redis instance, update host and port. To enable Redis persistence, configure RDB snapshots or AOF in your redis.conf.
© 2026 Yogesh Shende. All Rights Reserved.