A Spring Boot backend for a community-driven Q&A platform.
- Achieved ~90% reduction in search latency
- MySQL avg: ~1338 ms → Elasticsearch avg: ~135 ms
- Used Transactional Outbox Pattern for reliable async indexing
Questly is a backend system for a Q&A platform (like Stack Overflow) supporting authentication, questions, answers, comments, tags, and personalized feeds.
It uses:
- MySQL as the transactional source of truth
- Elasticsearch for fast full-text search
A transactional outbox pattern ensures safe and consistent synchronization between the database and search index.
- JWT-based authentication (Spring Security)
- CRUD APIs for questions, answers, comments, tags
- Tag-based personalized feed
- Dual search:
- MySQL (baseline)
- Elasticsearch (optimized)
- Reindexing support
- Benchmarking scripts for performance comparison
| Layer | Tech |
|---|---|
| Backend | Spring Boot 3.5 |
| Language | Java 21 |
| Security | Spring Security + JWT |
| Database | MySQL 8 |
| Search | Elasticsearch |
| Build | Gradle |
| Infra | Docker |
- Controllers expose REST endpoints under
/api/authand/api/v1/**. - Services contain the business logic for authentication, questions, answers, comments, tags, and feeds.
- JPA repositories persist relational entities such as
User,Question,Answer,Comment, andTag. - Elasticsearch stores
QuestionDocumentrecords for full-text question search. OutboxEventrecords are written in the same MySQL transaction as question create/delete operations.- A scheduled publisher reads pending outbox events and applies them to Elasticsearch with retry support.
| Entity | Purpose |
|---|---|
User |
Authenticated platform user with role and followed tags |
Question |
Main question entity with title, content, tags, author, and answers |
Answer |
Answer attached to a question and authored by a user |
Comment |
Comment attached to an answer, with optional nested reply structure |
Tag |
Topic label used for organizing questions and building user feeds |
QuestionDocument |
Elasticsearch representation of a question for search |
OutboxEvent |
Pending or processed event used to sync question changes to Elasticsearch |
1. Authentication Flow
- Client sends
POST /api/auth/registerorPOST /api/auth/login. AuthServiceImplvalidates the request and credentials.- Server issues a signed JWT in the response.
- Client sends
Authorization: Bearer <token>on protected requests. JwtAuthFiltervalidates the token and sets the Spring Security context.
2. Create Question (Transactional Outbox)
- Client sends
POST /api/v1/questions. QuestionServiceImplsaves the question to MySQL.- Atomic Write:
OutboxEventis saved in the same transaction. - Publisher syncs the question to Elasticsearch asynchronously.
3. Delete Question (Transactional Outbox)
- Client sends
DELETE /api/v1/questions/{id}. QuestionServiceImpldeletes the question from MySQL.- Atomic Write: delete
OutboxEventis saved in the same transaction. - Publisher removes the question document from Elasticsearch asynchronously.
4. Elasticsearch Search Flow
- Client sends
GET /api/v1/questions/search?query=.... QuestionSearchServiceImplqueries Elasticsearch.- Matching
QuestionDocumentrecords are returned to the client.
5. Database Search Flow
- Client sends
GET /api/v1/questions/search/db?query=.... QuestionServiceImplperforms the search against MySQL.- Matching
Questionentities are returned to the client.
6. Tag Follow and Feed Flow
- Client sends
POST /api/v1/users/{userId}/followTag/{tagId}. - User-tag relationship is stored in MySQL.
- Client requests
GET /api/v1/feed/{userId}?page=...&size=.... - Service loads followed tags and returns matching questions for the feed.
Questly-Backend/
├── src/main/java/com/example/questlybackend/
│ ├── controllers/
│ ├── dtos/
│ ├── models/
│ ├── repositories/
│ ├── security/
│ ├── services/
│ └── services/impl/
├── src/main/resources/
│ └── application.properties
├── src/test/java/
├── scripts/
├── BENCHMARKING.md
├── build.gradle
└── readme.md
- Java 21+
- Gradle wrapper support via
./gradlew - MySQL 8 running locally
- Docker for running Elasticsearch locally
git clone https://github.com/reshwanthind/Questly-Backend
cd Questly-BackendElasticsearch is expected to run through Docker for local development. The following command starts a single-node Elasticsearch instance, exposes ports 9200 and 9300, and disables security settings to keep local development simple.
docker run -d --name elasticsearch-container \
-p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
-e "xpack.security.http.ssl.enabled=false" \
elasticsearch:9.0.2Make sure your MySQL instance is running and matches the credentials configured in application.properties.
./gradlew bootRunThe API will start on http://localhost:8080.
All endpoints except /api/auth/** require a JWT bearer token.
🔐 Auth APIs
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/auth/register |
Register a new user and return a JWT |
POST |
/api/auth/login |
Authenticate an existing user and return a JWT |
👤 User APIs
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/users |
List all users |
GET |
/api/v1/users/{id} |
Get a user by ID |
POST |
/api/v1/users |
Create a user |
DELETE |
/api/v1/users/{id} |
Delete a user |
POST |
/api/v1/users/{userId}/followTag/{tagId} |
Follow a tag |
🏷️ Tag APIs
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/tags |
List all tags |
GET |
/api/v1/tags/{id} |
Get a tag by ID |
POST |
/api/v1/tags |
Create a tag |
DELETE |
/api/v1/tags/{id} |
Delete a tag |
❓ Question APIs
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/questions?page=0&size=10 |
Get paginated questions |
GET |
/api/v1/questions/{id} |
Get a question by ID |
POST |
/api/v1/questions |
Create a new question |
DELETE |
/api/v1/questions/{id} |
Delete a question |
GET |
/api/v1/questions/search?query=... |
Search questions through Elasticsearch |
GET |
/api/v1/questions/search/db?query=... |
Search questions through MySQL |
POST |
/api/v1/questions/search/reindex |
Rebuild the Elasticsearch question index from MySQL |
💬 Answer APIs
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/answers/{id} |
Get an answer by ID |
GET |
/api/v1/answers/question/{questionId}?page=0&size=10 |
Get answers for a question |
POST |
/api/v1/answers |
Create an answer |
DELETE |
/api/v1/answers/{id} |
Delete an answer |
🗨️ Comment APIs
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/comments/{id} |
Get a comment by ID |
GET |
/api/v1/comments/answer/{answerId}?page=0&size=10 |
Get comments for an answer |
GET |
/api/v1/comments/comment/{commentId}?page=0&size=10 |
Get replies for a parent comment |
POST |
/api/v1/comments |
Create a comment |
DELETE |
/api/v1/comments/{id} |
Delete a comment |
📰 Feed APIs
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/feed/{userId}?page=0&size=10 |
Get a user feed based on followed tags |
👉 Example Request & Response: API_Example.md
Elasticsearch significantly outperforms MySQL for search workloads.
| Metric | MySQL Search | Elasticsearch Search |
|---|---|---|
| Average latency | 1338.49 ms |
135.05 ms |
| Median latency | 1332.76 ms |
132.81 ms |
Average latency reduction: 89.91%
👉 Full results: BENCHMARKING.md
🌱 Why Spring Boot?
Spring Boot makes it easier to build a layered backend with strong ecosystem support for REST APIs, security, JPA, and Elasticsearch integration.
🐬 Why MySQL?
MySQL is the transactional source of truth for the application. It stores the relational model and supports the core business operations around users, questions, answers, comments, and tags.
🔍 Why Elasticsearch?
Elasticsearch is optimized for full-text search. It is used here to handle question search separately from the transactional database so search workloads do not rely only on relational string matching.
🔐 Why JWT?
JWT allows the backend to remain stateless. Each request carries its own authentication token, which works well with Spring Security and avoids server-side session storage.
📦 Why a Transactional Outbox?
Implemented to solve the Dual Write Problem. It ensures Eventual Consistency without sacrificing the performance of the main request thread or risking data drift if the search cluster is temporarily unreachable.
📊 Why Benchmark Scripts?
The benchmark scripts make the search tradeoff measurable. They show the value of Elasticsearch using the same dataset, the same query, and the same application APIs.
This project is licensed under the MIT License.
