A RESTful API for managing customers and orders with advanced features including pagination, filtering, bulk import, and report generation to files.
- ✅ CRUD Operations for Customers and Orders
- ✅ Pagination & Sorting with custom validation
- ✅ Advanced Filtering for orders
- ✅ Bulk Import from JSON files using streaming parser for large files
- ✅ Report Generation in CSV and XLSX formats using memory-efficient streaming
- ✅ Custom Pageable Validation with whitelist/blacklist support
- ✅ Bean Validation for all DTOs
- ✅ Proper HTTP Status Codes and error handling
- Java 21+
- Spring Boot 3.x
- Spring Data JPA
- PostgreSQL and H2 for testing
- MapStruct for DTO mapping
- Apache POI for Excel generation
- Jackson for JSON processing
- Lombok for boilerplate reduction
This API implements a custom @PageableConstraints annotation for secure and controlled pagination and sorting.
The annotation provides three validation modes:
- Whitelist Mode - Only specified fields can be used for sorting
- Blacklist Mode - All fields except specified ones can be used for sorting
- No Sorting Mode - If both whitelist and blacklist are empty, sorting is completely disabled
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PageableConstraints {
String[] whitelist() default {};
String[] blacklist() default {};
}Whitelist Mode:
@GetMapping("/api/customers")
public CustomerListResponseDto getPageableList(
@PageableConstraints(whitelist = {"firstName", "lastName", "city"})
@PageableDefault(size = 5) Pageable pageable) {
return customerService.listCustomers(pageable);
}Blacklist Mode:
@GetMapping("/api/orders")
public OrderListResponseDto getPageableList(
@PageableConstraints(blacklist = {"password", "secretField"})
@PageableDefault(size = 5) Pageable pageable) {
return orderService.listOrders(pageable);
}No Sorting Allowed:
@GetMapping("/api/sensitive")
public ResponseDto getSensitiveData(
@PageableConstraints() // Both empty = no sorting allowed
@PageableDefault(size = 10) Pageable pageable) {
return service.list(pageable);
}The PageableConstraintResolver validates:
- Page: Must be >= 0
- Size: Must be >= 0
- Sort fields: Must comply with whitelist/blacklist rules
Invalid requests throw InvalidRequestParameterException with descriptive error messages.
GET /api/customers?page=0&size=10&sort=firstName,asc&sort=city,descQuery Parameters:
page(optional, default: 0) - Page numbersize(optional, default: 5) - Page sizesort(optional) - Sorting field and direction, can retrieve multiple sorting fields
Allowed Sort Fields: firstName, lastName, city
Response:
{
"customers": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phone": "+380501234567",
"city": "Kyiv"
}
],
"totalPages": 5
}GET /api/customers/{id}Path Parameters:
id(UUID) - Customer ID
Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phone": "+380501234567",
"city": "Kyiv"
}Error Responses:
404 Not Found- Customer doesn't exist
POST /api/customers
Content-Type: application/jsonRequest Body:
{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phone": "+380501234567",
"city": "Kyiv"
}Validation Rules:
firstName: required, 3-100 characterslastName: required, 3-100 charactersemail: required, valid email format, must be uniquephone: required, 3-50 characterscity: required, 3-255 characters
Response: 201 Created
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phone": "+380501234567",
"city": "Kyiv"
}Error Responses:
400 Bad Request- Validation errors or email already exists
PUT /api/customers/{id}
Content-Type: application/jsonPath Parameters:
id(UUID) - Customer ID
Request Body:
{
"firstName": "John Updated",
"lastName": "Doe",
"phone": "+380501234999",
"city": "Lviv"
}Note: Email cannot be updated through this endpoint.
Response: 200 OK
Error Responses:
404 Not Found- Customer doesn't exist400 Bad Request- Validation errors
DELETE /api/customers/{id}Path Parameters:
id(UUID) - Customer ID
Response: 200 OK
{
"status": 200,
"message": "Customer with id '123e4567-e89b-12d3-a456-426614174000' was deleted."
}Note: This will cascade delete all associated orders.
Error Responses:
404 Not Found- Customer doesn't exist
GET /api/orders?page=0&size=10&sort=amount,descQuery Parameters:
page(optional, default: 0) - Page numbersize(optional, default: 5) - Page sizesort(optional) - Sorting field and direction
Allowed Sort Fields: status, paymentMethod, amount
Response:
{
"orders": [
{
"id": "456e7890-e89b-12d3-a456-426614174000",
"amount": 100.50,
"status": "NEW",
"paymentMethod": "CARD",
"createdAt": "2025-12-08T10:30:00Z",
"customer": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phone": "+380501234567",
"city": "Kyiv"
}
}
],
"totalPages": 10
}GET /api/orders/{id}Path Parameters:
id(UUID) - Order ID
Response:
{
"id": "456e7890-e89b-12d3-a456-426614174000",
"amount": 100.50,
"status": "NEW",
"paymentMethod": "CARD",
"createdAt": "2025-12-08T10:30:00Z",
"customer": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phone": "+380501234567",
"city": "Kyiv"
}
}Error Responses:
404 Not Found- Order doesn't exist
POST /api/orders
Content-Type: application/jsonRequest Body:
{
"customerId": "123e4567-e89b-12d3-a456-426614174000",
"amount": 100.50,
"status": "NEW",
"paymentMethod": "CARD"
}Validation Rules:
customerId: required, valid UUID format, customer must existamount: required, must be positivestatus: required, valid values:NEW,PROCESSING,DONE,CANCELEDpaymentMethod: required, valid values:CARD,CASH,PAYPAL,GOOGLE_PAY,APPLE_PAY
Response: 201 Created
Error Responses:
404 Not Found- Customer doesn't exist400 Bad Request- Validation errors or invalid enum values
PUT /api/orders/{id}
Content-Type: application/jsonPath Parameters:
id(UUID) - Order ID
Request Body:
{
"amount": 150.75,
"status": "PROCESSING",
"paymentMethod": "PAYPAL"
}Note: Customer assignment cannot be changed through this endpoint.
Response: 200 OK
Error Responses:
404 Not Found- Order doesn't exist400 Bad Request- Validation errors
POST /api/orders/_list
Content-Type: application/jsonRequest Body:
{
"customerId": "123e4567-e89b-12d3-a456-426614174000",
"status": "NEW",
"paymentMethod": "CARD",
"page": 0,
"size": 10
}Important Notes:
- Pagination parameters (
pageandsize) are passed in the request body, not as query parameters - All filter parameters are optional
- Returns orders in short format (reduced customer information)
Request Body Parameters:
customerId(optional) - Filter by specific customer (valid UUID)status(optional) - Filter by order statuspaymentMethod(optional) - Filter by payment methodpage(optional, default: 0, min: 0) - Page numbersize(optional, default: 5, min: 1, max: 100) - Page size
Response:
{
"orders": [
{
"id": "456e7890-e89b-12d3-a456-426614174000",
"amount": 100.50,
"status": "NEW",
"paymentMethod": "CARD",
"createdAt": "2025-12-08T10:30:00Z",
"customer": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"fullName": "John Doe",
"email": "john.doe@example.com"
}
}
],
"totalPages": 3
}Error Responses:
404 Not Found- Specified customer doesn't exist400 Bad Request- Validation errors
POST /api/orders/_report
Content-Type: application/jsonRequest Body:
{
"customerId": "123e4567-e89b-12d3-a456-426614174000",
"status": "DONE",
"paymentMethod": "CARD",
"fileType": "xlsx"
}Request Body Parameters:
customerId(optional) - Filter by specific customer (valid UUID)status(optional) - Filter by order statuspaymentMethod(optional) - Filter by payment methodfileType(optional, default: "csv") - Output format:csvorxlsx
Response: Binary file download
Response Headers:
Content-Type: text/csv
or
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Disposition: attachment; filename="orders_report_20251208_103000.csv"
Cache-Control: no-cache, no-store, must-revalidate
Report Columns:
- Order ID
- Customer ID
- Customer Name
- Amount
- Status
- Payment Method
- Created At
Features:
- Memory-efficient streaming (handles large datasets)
- CSV format: proper escaping for commas, quotes, newlines
- XLSX format: styled headers, bordered cells, numeric formatting
POST /api/orders/upload
Content-Type: multipart/form-dataYou can use prepared file that contains orders records with customerId's. Liquibase creates some customers
with certain id's. So after running the application you can use for POST /api/orders/upload json from
src/main/resources/jsonFiles/sampleOrdersImport.json.
Request:
- Form field:
file(JSON file)
File Requirements:
- Format: JSON (.json extension)
- Maximum size: 10MB
- Root element: array
Expected JSON Format:
[
{
"customerId": "123e4567-e89b-12d3-a456-426614174000",
"amount": 100.50,
"status": "NEW",
"paymentMethod": "CARD"
},
{
"customerId": "123e4567-e89b-12d3-a456-426614174000",
"amount": 250.00,
"status": "PROCESSING",
"paymentMethod": "PAYPAL"
}
]Validation per Order:
customerId: required, valid UUID, customer must existamount: required, must be positivestatus: required, valid enumpaymentMethod: required, valid enum
Response: 200 OK
{
"totalRecords": 100,
"successfulImports": 95,
"failedImports": 5,
"errors": [
{
"lineNumber": 23,
"reason": "Customer not found",
"details": "No customer with ID: 999e4567-e89b-12d3-a456-426614174000"
},
{
"lineNumber": 45,
"reason": "Invalid amount",
"details": "Amount must be positive, got: -10.00"
}
]
}Features:
- Streaming JSON parser (memory-efficient)
- Batch processing (50 records per batch)
- Detailed error reporting with line numbers
- Partial success (valid records are saved even if some fail)
Error Responses:
400 Bad Request- File validation errors (empty, wrong format, too large)
DELETE /api/orders/{id}Path Parameters:
id(UUID) - Order ID
Response: 200 OK
{
"status": 200,
"message": "Order with id '456e7890-e89b-12d3-a456-426614174000' was deleted."
}Error Responses:
404 Not Found- Order doesn't exist
All endpoints return consistent error responses:
{
"timestamp": "2025-12-08T10:30:00Z",
"status": 400,
"message": "Validation failed"
}{
"timestamp": "2025-12-08T10:30:00Z",
"status": 404,
"message": "Customer with id '123e4567-e89b-12d3-a456-426614174000' not found"
}{
"timestamp": "2025-12-08T10:30:00Z",
"status": 400,
"message": "Sorting by field 'city' is not allowed. Allowed fields: [status, paymentMethod, amount]"
}CREATE TABLE customers
(
id UUID PRIMARY KEY,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(50),
city VARCHAR(255),
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);CREATE TABLE orders
(
id UUID PRIMARY KEY,
customer_id UUID NOT NULL REFERENCES customers (id) ON DELETE CASCADE,
amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(50) NOT NULL,
payment_method VARCHAR(50),
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);- Java 21 or higher
- PostgreSQL (H2 for testing)
- Maven
- Docker and Docker Compose (recommended) or PostgreSQL installed locally
This project uses Liquibase for database migrations and seed data, which is why spring.jpa.hibernate.ddl-auto is
set to none. The database schema is managed entirely by Liquibase, not Hibernate.
1. Start the Database Container
docker-compose up -dThis will:
- Start PostgreSQL on port
5433(host) →5432(container) - Create the database with credentials from
docker-compose.yml
2. Verify Container is Running
docker-compose ps3. Run the Application
mvn spring-boot:runLiquibase will automatically:
- Create database schema
- Insert seed data
- Apply any pending migrations
If you prefer to use a local PostgreSQL installation:
1. Create Database
CREATE
DATABASE orders;2. Update Configuration
Modify application.properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/ordersNote:
- Change port from
5433(Docker) to5432(local PostgreSQL) - Liquibase will handle all migrations and seed data.
3. Run the Application
mvn spring-boot:runThe API will be available at http://localhost:8080
mvn testmvn test -Dtest=*IntegrationTestThe project includes comprehensive integration tests for:
- All controller endpoints
- CSV report generation
- XLSX report generation
- Pagination and filtering
- Validation scenarios
- Error handling
Once the application is running, you can access:
- Swagger UI:
http://localhost:8080/swagger-ui
- Report Generation uses streaming queries (Hibernate) and streaming writers (POI SXSSFWorkbook) to handle millions of records without OutOfMemoryError
- File uploading uses Jackson streaming parser to process large JSON files (up to 10MB like in configuration, you can change that value) without loading entire file into memory
- Batch Processing import processes records in batches of 50 for optimal database performance
- Proper indexes on foreign keys and frequently queried fields
@EntityGraphfor efficient eager loading and avoiding N+1 queries- Read-only transactions for query operations
- Query hints for streaming (fetch size, read-only)
This project is licensed under the MIT License.
Halmber