Backend service for a chat/messaging system. It provides REST APIs for conversations, messages, read/delivery state, and attachment upload flow, plus STOMP over WebSocket for real-time message delivery.
| Area | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 4.0.5 |
| Build tool | Gradle |
| REST API | Spring Web / Spring MVC |
| WebSocket | Spring WebSocket + STOMP + SockJS |
| Persistence | Spring Data JPA, Hibernate |
| Database | PostgreSQL 16 |
| Migrations | Liquibase |
| Cache / WebSocket tickets | Redis |
| Messaging | RabbitMQ, Spring AMQP |
| Object storage | AWS S3 SDK v2 |
| API documentation | springdoc-openapi / Swagger UI |
| Mapping | MapStruct |
| Validation | Jakarta Bean Validation |
| Boilerplate reduction | Lombok |
| Testing | Spock, Groovy, Spring Boot Test |
| Containerization | Docker, Docker Compose |
| Code quality | Checkstyle |
- Create conversations with multiple participants.
- List conversations for a user with cursor pagination.
- Send text, image, or file messages.
- Fetch messages for a conversation with pagination.
- Track delivered and read message state.
- Send and receive chat events over WebSocket/STOMP.
- Generate S3 presigned upload URLs for attachments.
- Confirm uploaded attachments by checking S3 object metadata.
- Publish message notification events to RabbitMQ after database commit.
- Manage schema changes through Liquibase migrations.
.
+-- src/main/java/com/messaging/chat
| +-- config # WebSocket, RabbitMQ, S3, OpenAPI configuration
| +-- controller # REST and WebSocket controllers
| +-- dao # JPA entities and repositories
| +-- mapper # MapStruct mappers
| +-- model # DTOs, constants, exceptions
| +-- service # Business logic
| +-- util # Utility classes
+-- src/main/resources
| +-- application.yaml
| +-- db/changelog # Liquibase changelogs
+-- build.gradle
+-- Dockerfile
+-- docker-compose.yml
- Java 21
- Docker and Docker Compose
- Gradle wrapper included in the project
- Optional: an AWS S3 bucket, LocalStack, or MinIO-compatible setup for attachment upload testing
Build and start all Compose services:
docker compose up --buildThe Compose file starts:
| Service | URL / Port |
|---|---|
| Chat service | http://localhost:8080 |
| PostgreSQL | localhost:5432 |
| Redis | localhost:6379 |
| RabbitMQ AMQP | localhost:5672 |
| RabbitMQ Management UI | http://localhost:15672 |
RabbitMQ default credentials:
guest / guest
Stop services:
docker compose downStop services and remove volumes:
docker compose down -vStart infrastructure only:
docker compose up postgres redis rabbitmqThen run the app from the project root:
./gradlew bootRunOn Windows PowerShell:
.\gradlew.bat bootRunBy default, application.yaml points PostgreSQL to localhost:5432. When the app runs inside Docker, docker-compose.yml overrides this to use the Compose service name postgres.
Spring Boot loads configuration from src/main/resources/application.yaml. Environment variables from Docker Compose override those values at startup through Spring Boot externalized configuration.
For example:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/chatmaps to:
spring:
datasource:
url: jdbc:postgresql://postgres:5432/chatImportant environment variables used by chat-service:
| Environment variable | Spring property | Purpose |
|---|---|---|
SPRING_DATASOURCE_URL |
spring.datasource.url |
PostgreSQL JDBC URL |
SPRING_DATASOURCE_USERNAME |
spring.datasource.username |
PostgreSQL username |
SPRING_DATASOURCE_PASSWORD |
spring.datasource.password |
PostgreSQL password |
SPRING_RABBITMQ_HOST |
spring.rabbitmq.host |
RabbitMQ host |
SPRING_RABBITMQ_PORT |
spring.rabbitmq.port |
RabbitMQ port |
SPRING_RABBITMQ_USERNAME |
spring.rabbitmq.username |
RabbitMQ username |
SPRING_RABBITMQ_PASSWORD |
spring.rabbitmq.password |
RabbitMQ password |
SPRING_DATA_REDIS_HOST |
spring.data.redis.host |
Redis host |
SPRING_DATA_REDIS_PORT |
spring.data.redis.port |
Redis port |
STORAGE_S3_BUCKET_NAME |
storage.s3.bucket-name |
S3 bucket for attachments |
STORAGE_S3_REGION |
storage.s3.region |
S3 region |
STORAGE_S3_ACCESS_KEY_ID |
storage.s3.access-key-id |
S3 access key |
STORAGE_S3_SECRET_ACCESS_KEY |
storage.s3.secret-access-key |
S3 secret key |
RABBITMQ_NOTIFICATION_QUEUE |
rabbitmq.notification-queue |
Notification queue name |
RABBITMQ_NOTIFICATION_EXCHANGE |
rabbitmq.notification-exchange |
Notification exchange name |
RABBITMQ_NOTIFICATION_ROUTING_KEY |
rabbitmq.notification-routing-key |
Notification routing key |
RABBITMQ_NOTIFICATION_DLQ |
rabbitmq.notification-dlq |
Dead-letter queue name |
RABBITMQ_NOTIFICATION_DLX |
rabbitmq.notification-dlx |
Dead-letter exchange name |
Note: docker-compose.yml does not currently start an S3-compatible service. The S3 values in Compose are placeholders unless you connect them to a real S3 bucket or add a local S3-compatible service such as LocalStack or MinIO.
When the app is running, OpenAPI documentation is available at:
http://localhost:8080/swagger-ui/index.html
The generated OpenAPI JSON is available at:
http://localhost:8080/v3/api-docs
All REST endpoints use the userId request header to identify the current user.
Create a conversation:
POST /ms-chat/conversations
userId: 1
Content-Type: application/json
{
"participantUserIds": [2, 3]
}List conversations:
GET /ms-chat/conversations?limit=20&cursor=50
userId: 1List messages in a conversation:
GET /ms-chat/conversations/20/messages?limit=30&beforeMessageId=171
userId: 1Send a message:
POST /ms-chat/conversations/20/messages
userId: 1
Content-Type: application/json
{
"clientMessageId": "550e8400-e29b-41d4-a716-446655440000",
"type": "TEXT",
"textContent": "Hello",
"attachmentIds": []
}Mark messages delivered:
POST /ms-chat/conversations/20/delivery
userId: 1
Content-Type: application/json
{
"messageId": 100
}Mark messages read:
POST /ms-chat/conversations/20/read
userId: 1
Content-Type: application/json
{
"messageId": 100
}Supported message types:
TEXT, IMAGE, FILE
Create a presigned upload URL:
POST /ms-chat/attachments/presign-upload
userId: 1
Content-Type: application/json
{
"fileName": "photo.png",
"contentType": "image/png",
"fileSize": 1048576,
"checksum": "1B2M2Y8AsgTpgAmY7PhCfg=="
}The response contains:
attachmentIdstorageKeyuploadUrl- HTTP method, currently
PUT - expiration time
- required upload headers
After uploading the file to S3 with the returned URL, confirm the attachment:
POST /ms-chat/attachments/10/confirm
userId: 1Attachment rules:
- Maximum file size is less than 5 MB.
- Executable content types are rejected.
- Executable file extensions are rejected.
- Confirmation checks that the uploaded S3 object size and content type match the presigned request.
WebSocket endpoint:
/ws-chat
SockJS is enabled.
The connection requires a query parameter:
/ws-chat?ticket=<ticket>
The ticket is resolved from Redis using this key format:
chat:ws-ticket:<ticket>
The Redis value must be the user id. The ticket is consumed with GETDEL, so it is single-use.
Application destination prefix:
/app
User destination prefix:
/user
Client sends messages to:
| Action | STOMP destination |
|---|---|
| Send message | /app/conversations/{conversationId}/messages |
| Mark delivered | /app/conversations/{conversationId}/delivery |
| Mark read | /app/conversations/{conversationId}/read |
Client subscribes to user queues:
| Event | STOMP subscription |
|---|---|
| New messages | /user/queue/messages |
| Delivery updates | /user/queue/delivery |
| Read updates | /user/queue/read |
When a message is sent, the service publishes notification events after the database transaction commits.
Configured RabbitMQ components:
- Notification queue
- Topic exchange
- Routing key binding
- Dead-letter queue
- Dead-letter exchange
The queue and exchange names are configured through rabbitmq.notification-* properties.
Liquibase runs automatically on application startup.
Master changelog:
src/main/resources/db/changelog/db.changelog-master.yaml
The application uses:
spring:
jpa:
hibernate:
ddl-auto: noneThat means Hibernate does not create/update schema automatically; schema changes should be added through Liquibase changelogs.
Build the project:
./gradlew buildRun tests:
./gradlew testRun Checkstyle:
./gradlew checkstyleMain checkstyleTestCreate the Spring Boot jar:
./gradlew bootJarOn Windows PowerShell, use .\gradlew.bat instead of ./gradlew.
The Dockerfile uses a multi-stage build:
eclipse-temurin:21-jdkbuilds the Spring Boot jar.eclipse-temurin:21-jreruns the jar.
The app listens on port 8080.
| Tool | URL |
|---|---|
| Application | http://localhost:8080 |
| Swagger UI | http://localhost:8080/swagger-ui/index.html |
| OpenAPI JSON | http://localhost:8080/v3/api-docs |
| Actuator health | http://localhost:8080/actuator/health |
| RabbitMQ UI | http://localhost:15672 |
- REST requests use the
userIdheader instead of a full authentication system. - WebSocket authentication uses a single-use Redis ticket.
- S3 presigned upload URLs expire after
storage.s3.presign-duration-minutes, currently configured as5. - Message notification publishing is transactional: RabbitMQ publishing runs after the database commit.
- Docker Compose service names are used for container-to-container networking, for example
postgres,redis, andrabbitmq.