Skip to content

reshwanthind/Questly-Backend

Repository files navigation

Questly 🧠

A Spring Boot backend for a community-driven Q&A platform.

Java Spring Boot Spring Security Elasticsearch MySQL Docker Gradle

🚀 Key Highlight

  • Achieved ~90% reduction in search latency
  • MySQL avg: ~1338 ms → Elasticsearch avg: ~135 ms
  • Used Transactional Outbox Pattern for reliable async indexing

📖 Overview

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.

✨ Core Features

  • 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

🛠️ Tech Stack

Layer Tech
Backend Spring Boot 3.5
Language Java 21
Security Spring Security + JWT
Database MySQL 8
Search Elasticsearch
Build Gradle
Infra Docker

🏗️ Architecture

Architecture Diagram

📝 Architecture Notes

  • Controllers expose REST endpoints under /api/auth and /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, and Tag.
  • Elasticsearch stores QuestionDocument records for full-text question search.
  • OutboxEvent records 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.

🧩 Domain Model

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

🔄 Key Workflows

1. Authentication Flow
  1. Client sends POST /api/auth/register or POST /api/auth/login.
  2. AuthServiceImpl validates the request and credentials.
  3. Server issues a signed JWT in the response.
  4. Client sends Authorization: Bearer <token> on protected requests.
  5. JwtAuthFilter validates the token and sets the Spring Security context.
2. Create Question (Transactional Outbox)
  1. Client sends POST /api/v1/questions.
  2. QuestionServiceImpl saves the question to MySQL.
  3. Atomic Write: OutboxEvent is saved in the same transaction.
  4. Publisher syncs the question to Elasticsearch asynchronously.
3. Delete Question (Transactional Outbox)
  1. Client sends DELETE /api/v1/questions/{id}.
  2. QuestionServiceImpl deletes the question from MySQL.
  3. Atomic Write: delete OutboxEvent is saved in the same transaction.
  4. Publisher removes the question document from Elasticsearch asynchronously.
4. Elasticsearch Search Flow
  1. Client sends GET /api/v1/questions/search?query=....
  2. QuestionSearchServiceImpl queries Elasticsearch.
  3. Matching QuestionDocument records are returned to the client.
5. Database Search Flow
  1. Client sends GET /api/v1/questions/search/db?query=....
  2. QuestionServiceImpl performs the search against MySQL.
  3. Matching Question entities are returned to the client.
6. Tag Follow and Feed Flow
  1. Client sends POST /api/v1/users/{userId}/followTag/{tagId}.
  2. User-tag relationship is stored in MySQL.
  3. Client requests GET /api/v1/feed/{userId}?page=...&size=....
  4. Service loads followed tags and returns matching questions for the feed.

📁 Project Structure

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

▶️ Running Locally

✅ Prerequisites

  • Java 21+
  • Gradle wrapper support via ./gradlew
  • MySQL 8 running locally
  • Docker for running Elasticsearch locally

1. 📥 Clone the Repository

git clone https://github.com/reshwanthind/Questly-Backend
cd Questly-Backend

2. 🐳 Start Elasticsearch with Docker

Elasticsearch 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.2

3. 🐬 Start MySQL

Make sure your MySQL instance is running and matches the credentials configured in application.properties.

4. ▶️ Run the Spring Boot Application

./gradlew bootRun

The API will start on http://localhost:8080.

🔌 API Overview

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

📊 Benchmark

Elasticsearch significantly outperforms MySQL for search workloads.

Local Result

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

🧠 Design Decisions

🌱 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.

📄 License

This project is licensed under the MIT License.

About

Spring Boot Q&A backend with JWT auth, RBAC, Elasticsearch full-text search, and Transactional Outbox Pattern for reliable indexing.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors