A RESTful blog platform built with Spring Boot 3
Features β’ Tech Stack β’ Getting Started β’ API Documentation β’ Project Highlights β’ Future Roadmap
- Overview
- Features
- Tech Stack
- Getting Started
- API Documentation
- Project Highlights
- Lessons Learned
- Areas for Improvement
- Future Roadmap (API v2)
- Author
BulagFaust is a full-featured blog platform backend that demonstrates enterprise-level Spring Boot development practices. The system provides complete CRUD operations for blog posts, categories, and tags, with role-based access control, JWT authentication, and production-ready error handling.
The name "BulagFaust" represents a journey from blindness (Bulag) to mastery (Faust) β symbolizing the learning process throughout this project's development.
- π Post Management - Create, read, update, and delete blog posts with draft/published status
- π·οΈ Tag System - Dynamic tag creation and assignment (auto-creates tags if they don't exist)
- π Category Organization - Hierarchical content categorization
- π€ User Authentication - JWT-based authentication with role-based authorization
- π Role-Based Access Control - ADMIN and USER roles with granular permissions
- Pagination & Sorting - Efficient data retrieval with customizable page sizes
- Filtering - Filter posts by category, tag, or author
- Reading Time Calculation - Automatic reading time estimation (200 WPM)
- Soft Validation - Comprehensive input validation with meaningful error messages
- Database Indexing - Optimized queries with strategic indexes on status, author, and timestamps
- N+1 Query Prevention - Batch loading with
@BatchSizefor efficient tag retrieval
| Category | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.2.4 |
| Security | Spring Security + JWT (JJWT 0.13.0) |
| Database | PostgreSQL |
| ORM | Spring Data JPA + Hibernate |
| Validation | Jakarta Validation API 3.1.1 |
| Object Mapping | MapStruct 1.6.3 |
| Boilerplate Reduction | Lombok 1.18.42 |
| Containerization | Docker + Docker Compose |
| Build Tool | Maven 3.x |
- Java 21 or higher
- Maven 3.6+
- Docker & Docker Compose (for database)
- Git
-
Clone the repository
git clone https://github.com/yourusername/BulagFaust.git cd BulagFaust -
Start the database with Docker Compose
docker-compose up -d
This starts:
- PostgreSQL on
localhost:5432 - Adminer (database UI) on
localhost:8081
- PostgreSQL on
-
Configure the application
Update
src/main/resources/application.propertiesif needed:spring.datasource.url=jdbc:postgresql://localhost:5432/mydb spring.datasource.username=postgres spring.datasource.password=postgres
-
Build and run
./mvnw clean install ./mvnw spring-boot:run
-
Access the application
- API Base URL:
http://localhost:8080/api/v1 - Default Admin:
admin@localhost/admin123
- API Base URL:
| Service | Host | Port | Username | Password |
|---|---|---|---|---|
| PostgreSQL | localhost | 5432 | postgres | postgres |
| Adminer | localhost | 8081 | postgres | postgres |
β οΈ Security Notice: Change default credentials in production!
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
POST |
/api/v1/auth/register |
Register new user | β |
POST |
/api/v1/auth/login |
User login | β |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/v1/posts |
Get all posts (paginated) | β |
GET |
/api/v1/posts/{id} |
Get post by ID | β |
POST |
/api/v1/posts |
Create new post | β |
PATCH |
/api/v1/posts/{id} |
Update post | β (Owner only) |
DELETE |
/api/v1/posts/{id} |
Delete post | β (Owner only) |
Query Parameters for GET /posts:
categoryId- Filter by category UUIDtagId- Filter by tag UUIDpage- Page number (default: 0)size- Page size (default: 20)sort- Sort field (default: createdAt, DESC)
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/v1/categories |
Get all categories | β |
POST |
/api/v1/categories |
Create category | β (ADMIN only) |
PATCH |
/api/v1/categories/{id} |
Update category | β (ADMIN only) |
DELETE |
/api/v1/categories/{id} |
Delete category | β (ADMIN only) |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/v1/tags |
Get all tags | β |
POST |
/api/v1/tags |
Create tag | β |
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "johndoe",
"email": "john@example.com",
"password": "securepass123"
}'Response:
{
"timestamp": "2026-03-03T10:30:00",
"status": 201,
"message": "Registered successfully",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "uuid-here",
"username": "johndoe",
"email": "john@example.com"
}
}
}curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "securepass123"
}'curl -X POST http://localhost:8080/api/v1/posts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"title": "My First Blog Post",
"content": "This is the content of my amazing post...",
"categoryIds": ["category-uuid-here"],
"tagNames": ["spring", "java", "backend"]
}'curl -X GET "http://localhost:8080/api/v1/posts?size=10&page=0&sort=createdAt,desc"curl -X PATCH http://localhost:8080/api/v1/posts/{post-id} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"title": "Updated Title",
"status": "PUBLISHED"
}'com.sysc.bulag_faust/
βββ auth/ # Authentication & authorization
βββ category/ # Category management
βββ post/ # Post business logic
βββ tag/ # Tag management
βββ user/ # User management
βββ role/ # Role-based access
βββ core/ # Cross-cutting concerns
βββ exceptions/
βββ security/
βββ response/
βββ utils/
Why it matters: Each module is self-contained, making the codebase maintainable and testable.
- Batch Loading: Used
@BatchSize(size = 20)to prevent N+1 queries when loading tags - Strategic Indexing: Added indexes on frequently queried columns (
status,author_id,created_at) - Pagination: All list endpoints support pagination to handle large datasets
// Example: Efficient tag resolution - single SELECT for all tags
private Set<Tag> resolveOrCreateTags(Set<String> tagNames) {
List<Tag> existing = tagRepository.findAllByNameIn(normalized);
// Only INSERT new tags, avoiding duplicate queries
}- Centralized
@RestControllerAdvicewith 15+ exception handlers - Consistent error response format with timestamps
- Appropriate HTTP status codes for different error scenarios
- Detailed logging for debugging without exposing sensitive info
- JWT token-based authentication (stateless)
- BCrypt password hashing
- Role-based authorization (
hasRole("ADMIN"),authenticated()) - CORS configuration for frontend integration
- SQL injection prevention via JPA parameterized queries
- Rich domain models with business logic encapsulated in entities
@PrePersistand@PreUpdatefor automatic timestamp management- Entity validation methods (
validateForPublish()) - Immutable DTOs with Java records
- Automatic reading time calculation (200 WPM algorithm)
- Lazy loading for relationships
open-in-view=falseto prevent session leaks- Efficient batch operations for tag creation
Before: Entities were just data containers with getters/setters.
Now: Entities encapsulate business logic:
// Post entity handles its own validation
public void publish() {
this.status = PostStatus.PUBLISHED;
validateForPublish(); // Self-validation
}Why: Reduces service layer bloat and keeps logic close to data.
Before: Manual timestamp and reading time calculation in services.
Now: Using JPA lifecycle callbacks:
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
setReadingTime(); // Automatic
}Why: DRY principle - logic defined once, triggered automatically.
Before: Throwing IllegalArgumentException everywhere.
Now: Custom exception hierarchy:
throw new NotFoundException("Post", id);
throw new AlreadyExistException("User", email);Why: Clearer intent, consistent error responses, easier debugging.
Before: FetchType.EAGER on all relationships.
Now: FetchType.LAZY with @BatchSize for collections:
@BatchSize(size = 20)
private Set<Tag> tags;Why: Prevents performance issues with large datasets.
Before: Manual DTO-to-entity conversion.
Now: Type-safe compile-time mapping with MapStruct:
@Mapper
public interface PostMapper {
PostResponse toResponse(Post post);
}Why: Reduces boilerplate, catches mapping errors at compile time.
Before: All logic in controllers.
Now: Clear separation:
Controller β Service β Repository β Entity
β β β β
HTTP Business Data Domain
Logic Logic Access Logic
Why: Testability, maintainability, single responsibility.
- β No unit tests for services
- β No integration tests for controllers
- β No test containers for database testing
- β No mock objects for external dependencies
Impact: Risk of regressions, harder refactoring.
- β No Redis or second-level cache
- β Repeated database queries for frequently accessed data
- β No cache invalidation strategy
Impact: Slower response times under load.
- β No rate limiting on authentication endpoints
- β No account lockout after failed attempts
- β No brute force protection
- β No request throttling
Impact: Vulnerable to DoS and brute force attacks.
β οΈ Only v1 endpoints existβ οΈ No backward compatibility strategyβ οΈ Breaking changes would require immediate migration
Impact: Difficult to evolve API without disrupting clients.
- β No OpenAPI/Swagger documentation
- β No API contract testing
- β Minimal inline documentation
Impact: Harder for frontend teams to integrate.
- β No Flyway/Liquibase for version control
- β Manual schema management
- β No rollback strategy
Impact: Risk of schema drift between environments.
β οΈ Basic logging with SLF4J- β No structured logging (JSON)
- β No metrics collection (Micrometer)
- β No distributed tracing
Impact: Difficult to debug production issues.
β οΈ No SonarQube integrationβ οΈ No code coverage reportsβ οΈ Inconsistent exception messages
| Feature | Why |
|---|---|
| Redis Rate Limiting | Prevent API abuse and DDoS attacks by limiting requests per user/IP |
| Account Lockout Mechanism | Protect against brute force attacks by locking accounts after failed login attempts |
| Redis Caching | Reduce database load and improve response times for frequently accessed data |
| Feature | Why |
|---|---|
| JUnit 5 | Write unit tests to verify business logic and catch regressions early |
| Mockito | Mock dependencies for isolated, fast unit testing |
| TestContainers | Run integration tests against real PostgreSQL instances in Docker |
| MockMvc | Test REST controllers without starting the full server |
| Feature | Why |
|---|---|
| Comments System | Enable user engagement and discussions on blog posts |
| Reactions/Likes | Allow readers to express appreciation without writing comments |
| User Analytics | Provide insights on post performance and reader engagement |
| Image Upload | Support rich content with embedded images in posts |
| Full-Text Search | Help users find relevant content quickly across all posts |
| Feature | Why |
|---|---|
| Flyway/Liquibase | Version control database schema changes and enable safe rollbacks |
| OpenAPI/Swagger | Auto-generated API documentation for easier frontend integration |
{
"timestamp": "2026-03-03T10:30:00",
"status": 200,
"message": "Retrieved all posts",
"data": {
"content": [...],
"pageable": {...},
"totalElements": 100,
"totalPages": 5
}
}{
"timestamp": "2026-03-03T10:30:00",
"code": "NOT_FOUND",
"message": "Post not found with id: 123e4567-e89b-12d3-a456-426614174000"
}βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β Client β ββββββ> β Controller β ββββββ> β Service β
β (Frontend) β HTTP β (REST) β Call β (Business) β
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β
βΌ
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β PostgreSQL β <ββββββ β Repository β <ββββββ β Entity β
β (Database) β JPA β (JPA) β Query β (Domain) β
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
Developed by: Rem Project: BulagFaust - Spring Boot Blog Platform
This project is licensed under the MIT License - see the LICENSE file for details.
Built with β€οΈ using Spring Boot