Spring boot + MyBatis codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the RealWorld spec and API.
This codebase was created to demonstrate a fully fledged fullstack application built with Spring boot + Mybatis including CRUD operations, authentication, routing, pagination, and more.
For more information on how to this works with other frontends/backends, head over to the RealWorld repo.
The application uses Spring boot (Web, Mybatis).
- Use the idea of Domain Driven Design to separate the business term and infrastruture term.
- Use MyBatis to implement the Data Mapper pattern for persistence.
- Use CQRS pattern to separate the read model and write model.
And the code organize as this:
apiis the web layer to implement by Spring MVCcoreis the business model including entities and servicesapplicationis the high level services for query with the data transfer objectsinfrastructurecontains all the implementation classes as the technique details
This application follows a layered architecture implementing Domain-Driven Design (DDD) principles, CQRS (Command Query Responsibility Segregation), and the Data Mapper pattern to achieve clean separation of concerns, maintainability, and scalability.
The application follows several architectural patterns to ensure clean separation of concerns and maintainability:
The codebase is organized following DDD principles, separating business logic from infrastructure concerns. Domain entities (core layer) contain business rules and are independent of frameworks and persistence details.
Key DDD Concepts Applied:
- Domain Entities: Rich domain models with business logic (Article, User, Comment)
- Repository Pattern: Abstractions for data access (ArticleRepository, UserRepository)
- Domain Services: Business logic that doesn't belong to a single entity (AuthorizationService)
- Bounded Context: Clear boundaries between layers
- Ubiquitous Language: Domain terms used consistently throughout codebase
The application separates read and write operations:
- Commands (Write): Handled through domain repositories in the
corelayer - Queries (Read): Handled through query services in the
applicationlayer using optimized read models
This separation allows for:
- Optimized read queries without affecting write operations
- Different data models for reads and writes
- Better scalability and performance
- Independent optimization of read and write paths
CQRS Implementation:
Write Path (Command):
API β Domain Entity β Repository Interface β MyBatis Repository β Database
Read Path (Query):
API β Query Service β Read Service β MyBatis Mapper β Database β DTO
MyBatis implements the Data Mapper pattern, which provides a clean separation between domain objects and database tables. The mapper handles the mapping between objects and database rows.
Benefits:
- Domain objects don't know about database structure
- Database schema changes don't affect domain logic
- Mapping logic centralized in MyBatis XML mappers
- Clean separation between persistence and domain layers
The application follows a layered architecture with four main layers:
βββββββββββββββββββββββββββββββββββββββββββ
β API Layer β
β (REST Controllers, Request/Response) β
βββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββ
β Application Layer β
β (Query Services, DTOs, Use Cases) β
βββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββ
β Core Layer β
β (Domain Entities, Repositories, β
β Business Services) β
βββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββ
β Infrastructure Layer β
β (MyBatis Mappers, Repository β
β Implementations, External Services) β
βββββββββββββββββββββββββββββββββββββββββββ
Purpose: Entry point for HTTP requests, handles request/response formatting
Responsibilities:
- REST endpoints (Controllers)
- Request validation
- Response formatting
- Exception handling
- Security (JWT authentication)
Key Components:
ArticlesApi,CommentsApi,UsersApi, etc. - REST controllerssecurity/- Security configuration and JWT filterexception/- Custom exception handlers
Example Flow:
@RestController
public class ArticlesApi {
// Receives HTTP request β Validates β Delegates to services β Returns response
}Purpose: High-level orchestration and query services
Responsibilities:
- Query services for read operations
- Data Transfer Objects (DTOs)
- Business use case orchestration
- Pagination and filtering logic
Key Components:
ArticleQueryService,CommentQueryService,UserQueryService- Query servicesdata/- DTOs (ArticleData, UserData, CommentData, etc.)Page- Pagination utilities
Characteristics:
- Uses read services from infrastructure layer
- Returns DTOs optimized for API responses
- Handles complex queries and aggregations
Purpose: Domain model and business logic
Responsibilities:
- Domain entities (Article, User, Comment, etc.)
- Repository interfaces (abstractions)
- Domain services (AuthorizationService, JwtService)
- Business rules and validations
Key Components:
article/,user/,comment/,favorite/- Domain entities*Repositoryinterfaces - Repository contractsservice/- Domain services
Characteristics:
- Framework-independent
- Contains business logic
- No direct database dependencies
- Pure Java objects
Purpose: Technical implementations and external integrations
Responsibilities:
- MyBatis mappers and repository implementations
- Database access logic
- External service implementations
- Technical utilities
Key Components:
repository/- MyBatis implementations of core repositoriesmybatis/mapper/- MyBatis mapper interfacesmybatis/readservice/- Optimized read servicesservice/- Technical service implementations (JWT, encryption)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client Request β
β HTTP POST /articles β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API Layer (io.spring.api) β
β - ArticlesApi.createArticle() β
β - Request validation (@Valid) β
β - Security check (@AuthenticationPrincipal) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Core Layer (io.spring.core) β
β - Article entity created with business logic β
β - Slug generation β
β - Tag processing β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Infrastructure Layer (io.spring.infrastructure) β
β - MyBatisArticleRepository.save() β
β - ArticleMapper.insert() β
β - TagMapper operations β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Database (MySQL) β
β - INSERT into articles table β
β - INSERT into article_tags table β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application Layer (io.spring.application) β
β - ArticleQueryService.findById() β
β - ArticleReadService.findById() β
β - DTO creation (ArticleData) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API Response (JSON) β
β { "article": { ... } } β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Example: Creating an Article
1. HTTP POST /articles
Headers: Authorization: Token <jwt>
Body: { "article": { "title": "...", "body": "..." } }
β
2. ArticlesApi.createArticle()
- Validates request body (@Valid)
- Checks authentication (@AuthenticationPrincipal)
- Extracts user from security context
β
3. Creates Article domain entity
- Article constructor generates slug from title
- Processes tagList array
- Sets userId, timestamps
β
4. ArticleRepository.save(article)
- Interface call (core layer)
β
5. MyBatisArticleRepository.save()
- Checks if article exists (update vs insert)
- For new articles:
- Inserts tags if not exist
- Creates article-tag relationships
- Inserts article record
β
6. MyBatis Mapper (ArticleMapper)
- Executes SQL via MyBatis XML mapper
- Maps domain object to database rows
β
7. Database Transaction
- All operations in single transaction
- Commit on success, rollback on error
β
8. Return response
- Query service fetches created article
- Converts to DTO (ArticleData)
- Returns JSON response
Example: Fetching Article List
1. HTTP GET /articles?tag=react&offset=0&limit=20
β
2. ArticlesApi.getArticles()
- Extracts query parameters
- Optional authentication check
β
3. ArticleQueryService.findRecentArticles()
- Builds query parameters
- Handles pagination (Page object)
- Passes user for personalization
β
4. ArticleReadService (infrastructure)
- Optimized read service
- Executes complex SQL joins
- Fetches articles with author info
- Includes favorite counts
β
5. MyBatis Mapper (ArticleMapper)
- Executes SELECT query
- Joins multiple tables:
- articles β users (author)
- articles β article_favorites (count)
- articles β article_tags β tags
β
6. Database Query
- Returns result set
β
7. MyBatis Result Mapping
- Maps rows to DTOs (ArticleData)
- Handles nested objects (author, tags)
β
8. ArticleQueryService.enrichData()
- Adds user-specific data:
- Is favorited by current user?
- Is following author?
β
9. Returns ArticleDataList
- Contains articles array
- Contains articlesCount for pagination
β
10. JSON Response
{ "articles": [...], "articlesCount": 100 }
1. HTTP POST /users/login
Body: { "user": { "email": "...", "password": "..." } }
β
2. UsersApi.userLogin()
- Validates request
β
3. UserRepository.findByEmail()
- Finds user by email
β
4. EncryptService.check()
- Verifies password hash
β
5. JwtService.toToken()
- Generates JWT token
- Includes user ID as subject
β
6. UserQueryService.findById()
- Fetches user data
- Converts to DTO (UserData)
β
7. Returns UserWithToken
- Includes user data + token
β
8. Subsequent Requests
- Client includes: Authorization: Token <jwt>
β
9. JwtTokenFilter (intercepts all requests)
- Extracts token from header
- Validates token via JwtService
- Loads user from UserRepository
- Sets authentication in SecurityContext
β
10. Controller receives @AuthenticationPrincipal User
- User object available in all controllers
Write Operations:
- Repository methods annotated with
@Transactional - Ensures atomicity for multi-step operations
- Example: Creating article with tags requires multiple inserts
- Rollback on any failure
Read Operations:
- No transactions required (read-only)
- Optimized for performance
- Can use read replicas in production
- Spring Boot 2.7.18 - Application framework
- Spring MVC - Web layer
- Spring Security - Security and authentication
- Spring Validation - Request validation
- MyBatis 2.3.1 - SQL mapping framework
- MySQL 8.0.33 - Production database
- H2 Database - In-memory database for testing
- Flyway - Database migration tool
- JWT (JJWT 0.11.2) - Token-based authentication
- Spring Security - Authentication and authorization
- Custom JWT Filter - Token validation middleware
- Lombok - Reduce boilerplate code
- Joda Time - Date/time handling
- RestAssured - API testing (test scope)
- User registers/logs in via
/usersor/users/login - Server generates JWT token and returns it
- Client includes token in
Authorization: Token <jwt>header JwtTokenFilterintercepts requests and validates token- Spring Security sets authenticated user in security context
- Controllers access user via
@AuthenticationPrincipal
- AuthorizationService - Checks if user can perform operations
- Article/Comment operations require ownership verification
- Profile follow/unfollow requires authentication
The application uses MyBatis with SQL mapping files:
- Tables:
users,articles,comments,tags,article_tags,article_favorites,follows - Relationships maintained through join tables
- Flyway manages schema migrations
API β Application β Core β Infrastructure
- API depends on Application and Core
- Application depends on Core and Infrastructure
- Core has no dependencies (pure domain)
- Infrastructure implements Core interfaces
This ensures that:
- Business logic remains independent
- Infrastructure can be swapped without changing core
- Testing is easier with clear boundaries
Integration with Spring Security and add other filter for jwt token process.
The secret key is stored in application.properties.
It uses a H2 in memory database (for now), can be changed easily in the application.properties for any other database.
Before you begin, ensure you have the following installed:
- Java 17 (JDK 17 or higher)
- Gradle (or use the Gradle wrapper included in the project)
- MySQL (for production/local development, or H2 for testing)
To run the application locally, you need to set the following environment variables:
DB_USERNAME - username which will be used to connect to MySQL DB instance
DB_PASSWORD - password which will be used to connect to MySQL DB instance
DB_URL - url by which application can access MySQL (e.g., localhost)
DB_PORT - port by which application can access MySQL (e.g., 3306)
DB_NAME - database name which will be used to store informationWindows (PowerShell):
$env:DB_USERNAME="your_username"
$env:DB_PASSWORD="your_password"
$env:DB_URL="localhost"
$env:DB_PORT="3306"
$env:DB_NAME="your_database"Windows (Command Prompt):
set DB_USERNAME=your_username
set DB_PASSWORD=your_password
set DB_URL=localhost
set DB_PORT=3306
set DB_NAME=your_databaseLinux/Mac:
export DB_USERNAME=your_username
export DB_PASSWORD=your_password
export DB_URL=localhost
export DB_PORT=3306
export DB_NAME=your_databaseTo build the application, run:
./gradlew buildThis will compile the code, run tests, and create a JAR file in build/libs/.
To build without running tests:
./gradlew build -x testTo clean previous build artifacts:
./gradlew cleanRun the application directly using Gradle:
./gradlew bootRunMake sure you have set all required environment variables before running this command.
After building the application, you can run it using the generated JAR:
./gradlew build
java -jar ./build/libs/*.jarReplace * with the actual JAR filename if needed.
Once the application starts, you can verify it's running by:
-
Opening a browser and navigating to: http://localhost:8080/tags
-
Or using curl:
curl http://localhost:8080/tags
The application will be available at http://localhost:8080
The repository contains comprehensive test cases covering both API tests and repository tests.
To run all tests:
./gradlew testFor more detailed test output:
./gradlew test --infoTo run a specific test class:
./gradlew test --tests "com.example.YourTestClass"The tests include:
- API Tests: Testing REST endpoints using RestAssured and Spring MockMvc
- Repository Tests: Testing database operations using MyBatis test utilities
- Security Tests: Testing JWT authentication and authorization
Note: Tests use H2 in-memory database by default, so no MySQL setup is required for testing.
The API follows the RealWorld API specification. All endpoints are available at http://localhost:8080.
Note: Endpoints marked with π require authentication via JWT token in the Authorization header: Authorization: Token <your-token>
- POST
/users - Description: Register a new user account
- Authentication: Not required
- Request Body:
{ "user": { "email": "[email protected]", "username": "johndoe", "password": "password123" } } - Response: Returns user object with JWT token
- POST
/users/login - Description: Authenticate a user and receive a JWT token
- Authentication: Not required
- Request Body:
{ "user": { "email": "[email protected]", "password": "password123" } } - Response: Returns user object with JWT token
- GET
/user - Description: Get the currently authenticated user's information
- Authentication: Required
- PUT
/user - Description: Update the currently authenticated user's information
- Authentication: Required
- Request Body:
{ "user": { "email": "[email protected]", "username": "newusername", "password": "newpassword", "bio": "User bio", "image": "https://example.com/image.jpg" } } - Note: All fields are optional; only include fields you want to update
- GET
/tags - Description: Get a list of all tags used in articles
- Authentication: Not required
- Response: Returns array of tag strings
- GET
/articles - Description: Get a paginated list of articles
- Authentication: Optional (returns additional data if authenticated)
- Query Parameters:
offset(default: 0) - Number of articles to skiplimit(default: 20) - Number of articles to returntag(optional) - Filter by tagauthor(optional) - Filter by author usernamefavorited(optional) - Filter by username who favorited
- Example:
/articles?tag=react&offset=0&limit=10
- GET
/articles/feed - Description: Get articles from users you follow
- Authentication: Required
- Query Parameters:
offset(default: 0) - Number of articles to skiplimit(default: 20) - Number of articles to return
- POST
/articles - Description: Create a new article
- Authentication: Required
- Request Body:
{ "article": { "title": "How to build webapps that scale", "description": "Web development techniques", "body": "This is the body of the article", "tagList": ["react", "webdev", "tutorial"] } }
- GET
/articles/{slug} - Description: Get a single article by slug
- Authentication: Optional (returns additional data if authenticated)
- Path Parameters:
slug- Article slug (e.g., "how-to-build-webapps-that-scale")
- PUT
/articles/{slug} - Description: Update an article (must be the author)
- Authentication: Required
- Request Body:
{ "article": { "title": "Updated title", "description": "Updated description", "body": "Updated body" } } - Note: All fields are optional; only include fields you want to update
- DELETE
/articles/{slug} - Description: Delete an article (must be the author)
- Authentication: Required
- POST
/articles/{slug}/favorite - Description: Mark an article as favorited
- Authentication: Required
- DELETE
/articles/{slug}/favorite - Description: Remove an article from favorites
- Authentication: Required
- GET
/articles/{slug}/comments - Description: Get all comments for an article
- Authentication: Optional (returns additional data if authenticated)
- POST
/articles/{slug}/comments - Description: Create a new comment on an article
- Authentication: Required
- Request Body:
{ "comment": { "body": "Great article!" } }
- DELETE
/articles/{slug}/comments/{id} - Description: Delete a comment (must be the comment author or article author)
- Authentication: Required
- Path Parameters:
slug- Article slugid- Comment ID
- GET
/profiles/{username} - Description: Get a user's profile information
- Authentication: Optional (returns additional data if authenticated)
- Path Parameters:
username- Username of the profile to retrieve
- POST
/profiles/{username}/follow - Description: Follow a user
- Authentication: Required
- DELETE
/profiles/{username}/follow - Description: Unfollow a user
- Authentication: Required
The entry point address of the backend API is at http://localhost:8080, not http://localhost:8080/api as some of the frontend documentation suggests.
