A distributed real-time chat application built around a microservice architecture, with OAuth2 authentication, gRPC inter-service communication, event-driven messaging via RabbitMQ and a BFF gateway layer.
Built with Java 25, Spring Boot 4, Maven, and Docker.
- Microservice Architecture: Independently deployable services — BFF, AuthService, UserService, and MessageService.
- BFF Gateway: Single entry point for clients using Spring Cloud Gateway with session-based OAuth2 authentication.
- gRPC Communication: Typed, efficient inter-service communication using Spring gRPC.
- OAuth2 Authorization Server: Dedicated AuthService handling login, token issuance, and session management.
- RESTful API: Clean API endpoints for users and messages, exposed through the BFF.
- PostgreSQL Storage: Persistent storage for users and messages via Docker-managed Postgres instances.
- Event-driven Messaging: MessageService publishes to a RabbitMQ queue (
message-published) whenever a message is saved, enabling downstream consumers to react to new messages asynchronously. - Docker Compose: One-command local setup with all services and databases containerized.
- Minimalistic Chat UI: Simple web interface with login, registration, and a real-time chat view served by the BFF.
- Demo User: A
demo/demouser is created automatically on startup. Log in straight away without registering.
Client
│
▼
BFF (8080) ──── OAuth2 ───► AuthService (9000)
│ │
│ HTTP │ REST (user lookup)
│ ▼
├───────────────────────► UserService (8081, gRPC 9091) ──► userdb
│ ▲
│ HTTP │ gRPC
│ │
└───────────────────────► MessageService (8082) ──────────► messagedb
│
│
▼
RabbitMQ
- BFF is the single entry point — it authenticates requests via OAuth2 and proxies them to the appropriate service.
- AuthService handles login and token issuance, and calls UserService over REST to look up users.
- MessageService calls UserService over gRPC to resolve user data when handling messages.
- UserService exposes both a REST API (port 8081, consumed by BFF and AuthService) and a gRPC interface (port 9091, consumed by MessageService).
- MessageService publishes an event to RabbitMQ whenever a message is saved.
- UserService and MessageService each own a dedicated PostgreSQL database (
userdbandmessagedb), following the database-per-service pattern.
- Java 25
- Maven
- Docker and Docker Compose
git clone https://github.com/simonforsberg/relay.git
cd relayThe dev profile is active by default in all services and uses pre-configured local values. Override with SPRING_PROFILES_ACTIVE if needed.
Copy .env.example to .env and fill in the values — Docker Compose reads this file automatically.
cp .env.example .envPOSTGRES_PASSWORD is always required; set it to postgres when using the dev profile.
docker compose up -dThis starts PostgreSQL (userdb on 5432, messagedb on 5433) and RabbitMQ (5672, management UI on 15672).
Start each service with the dev profile active (set via SPRING_PROFILES_ACTIVE=dev or run configuration):
AuthServiceApplication— port9000UserServiceApplication— port8081MessageServiceApplication— port8082BffApplication— port8080
IDE: Run each main class directly with the dev profile.
Maven:
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev- Chat UI: http://localhost:8080
- RabbitMQ Management: http://localhost:15672 (
guest/guest)
The API can be tested using Insomnia or any HTTP client.
The BFF (localhost:8080) uses session-based OAuth2 — all requests through it
require a valid session cookie. To get one:
- Log into the chat at
http://localhost:8080with demo user credentials (demo/demo) - Open DevTools → Network → click any request to
localhost:8080 - Copy the
JSESSIONIDvalue from the Cookie header - Add it to your requests:
Cookie: JSESSIONID=<value>
The session is invalidated on logout or server restart, so you may need to repeat this step.
Public endpoints (no auth required) can be called directly on UserService:
| Method | URL | Description |
|---|---|---|
POST |
http://localhost:8081/api/users |
Register a new user |
GET |
http://localhost:8081/api/users/by-username?username={username} |
Look up a user by username |
All other endpoints go through the BFF and require the session cookie:
| Method | URL | Description |
|---|---|---|
GET |
http://localhost:8080/api/users |
Get all users |
GET |
http://localhost:8080/api/users/{id} |
Get user by ID |
GET |
http://localhost:8080/api/users/me |
Get the currently logged-in user |
PUT |
http://localhost:8080/api/users/{id} |
Update a user |
DELETE |
http://localhost:8080/api/users/{id} |
Delete a user |
GET |
http://localhost:8080/api/messages |
Get all messages |
GET |
http://localhost:8080/api/messages/{id} |
Get message by ID |
GET |
http://localhost:8080/api/messages/sender/{senderId} |
Get messages by sender |
POST |
http://localhost:8080/api/messages |
Send a message |
Register a user — POST /api/users
POST http://localhost:8081/api/users
Content-Type: application/json
{
"username": "ron-burgundy",
"password": "password123",
"email": "ron@example.com"
}Update a user — PUT /api/users/{id}
PUT http://localhost:8080/api/users/{id}
Content-Type: application/json
Cookie: JSESSIONID=<value>
{
"username": "ron-burgundy_updated",
"password": "newpassword123",
"email": "ron_new@example.com"
}Send a message — POST /api/messages
POST http://localhost:8080/api/messages
Content-Type: application/json
Cookie: JSESSIONID=<value>
{
"content": "Stay classy!"
}