Overview
The ecommerce API is built on Spring Boot and follows a layered, RESTful architecture. The application uses proven design patterns including Controller-Repository, dependency injection, and HATEOAS for hypermedia-driven responses.
Application Structure
The Spring Boot application is organized into domain-based packages, each containing all components related to a specific business entity.
Package Organization
Layer Responsibilities
com.example.ecommerce/
├── EcommerceApplication.java # Main Spring Boot application
├── DatabasePreLoader.java # Initial data seeding
├── product/ # Product domain
│ ├── Product.java # Entity
│ ├── ProductController.java # REST controller
│ ├── ProductRepository.java # Data access
│ ├── ProductAssembler.java # HATEOAS assembler
│ ├── ProductView.java # DTO for responses
│ └── ProductCategory.java # Join entity
├── user/ # User domain
│ ├── User.java
│ ├── UserController.java
│ ├── UserRepository.java
│ ├── UserAssembler.java
│ └── SecurityConfig.java
├── category/ # Category domain
│ ├── Category.java
│ ├── CategoryController.java
│ └── CategoryRepository.java
└── cart/ # Shopping cart domain
├── Cart.java
├── CartItems.java
├── CartRepository.java
└── CartItemsRepository.java
Controller Layer
Handles HTTP requests and responses
Maps endpoints to business logic
Performs request/response transformation
Returns HATEOAS-enriched resources
Repository Layer
Extends Spring Data JPA repositories
Provides database CRUD operations
Supports custom query methods
Manages entity persistence
Entity Layer
Defines JPA entities with annotations
Specifies relationships between entities
Contains validation rules
Maps to database tables
Assembler Layer
Converts entities to EntityModel resources
Adds hypermedia links to responses
Implements RepresentationModelAssembler
Decouples entity structure from API responses
Controller-Repository Pattern
The API follows the Controller-Repository pattern where controllers handle HTTP requests and delegate data operations to repositories.
Example: ProductController
@ RestController
public class ProductController {
private final ProductRepository productRepository ;
private final ProductAssembler productAssembler ;
private final PagedResourcesAssembler < ProductView > pagedResourceAssembler ;
// Constructor injection for dependencies
public ProductController (
ProductRepository productRepository ,
ProductAssembler productAssembler ,
PagedResourcesAssembler < ProductView > pagedResourceAssembler ,
CategoryRepository categoryRepository
) {
this . productRepository = productRepository;
this . productAssembler = productAssembler;
this . pagedResourceAssembler = pagedResourceAssembler;
}
@ GetMapping ( "/products" )
PagedModel < EntityModel < ProductView >> findAll ( Pageable pageable ) {
Page < Product > page = productRepository . findAll (pageable);
Page < ProductView > productViewPage = page . map (ProductView ::new );
return pagedResourceAssembler . toModel (productViewPage);
}
@ GetMapping ( "/products/{id}" )
EntityModel < ProductView > findOne (@ PathVariable Long id ) {
Product product = productRepository . findById (id)
. orElseThrow (() -> new ProductNotFoundException (id));
return productAssembler . toModel (product);
}
}
Key Points:
@RestController marks the class as a REST API controller
Dependencies injected via constructor (Spring’s recommended approach)
Repository handles data access, controller handles HTTP concerns
Assembler transforms entities into HATEOAS resources
Separation of Concerns : Controllers focus on HTTP handling while repositories manage persistence. This makes the code more testable and maintainable.
Database Layer
The application uses Spring Data JPA with Hibernate as the JPA implementation to interact with the database.
JPA Repositories
Repositories extend JpaRepository to get built-in CRUD operations and query methods:
public interface ProductRepository extends JpaRepository < Product , Long > {
// Spring Data JPA provides:
// - findAll(), findById(), save(), delete()
// - Custom query methods
// - Pagination and sorting
}
Entity Mapping
Entities use JPA annotations to map Java classes to database tables:
Basic Mapping
Relationships
@ Entity
public class Product {
@ Id
@ GeneratedValue
private Long id ;
private String name ;
private String description ;
private BigDecimal price ;
private Long stockQuantity ;
@ CreatedDate
@ Column ( nullable = false , updatable = false )
private LocalDateTime createdAt ;
@ UpdateTimestamp
private LocalDateTime updatedAt ;
}
Many-to-One (Product → User) @ ManyToOne
@ JoinColumn ( name = "user_id" )
private User user ;
One-to-Many (Product → ProductCategory) @ OneToMany ( mappedBy = "product" ,
cascade = CascadeType . ALL ,
orphanRemoval = true )
private List < ProductCategory > categories = new ArrayList <>();
Many-to-One with Lazy Loading (CartItems → Product) @ ManyToOne ( fetch = FetchType . LAZY , optional = false )
@ JoinColumn ( name = "product_id" , nullable = false )
private Product product ;
Spring Data JPA automatically generates SQL queries based on method names. For example, findByName(String name) will query products by name.
RESTful Design
The API follows REST principles with standard HTTP methods and status codes.
HTTP Methods
Method Endpoint Purpose Response GET/productsList all products 200 OK with PagedModelGET/products/{id}Get single product 200 OK with EntityModelPOST/productsCreate product 201 Created with Location headerPUT/PATCH/products/{id}Update product 200 OKDELETE/products/{id}Delete product 204 No Content
Resource Creation Example
@ PostMapping ( "/products" )
ResponseEntity < EntityModel < ProductView >> saveProduct (
@ RequestBody ProductCreateRequest newProductRequest
) {
Product product = new Product ( /* ... */ );
Product savedProduct = productRepository . save (product);
return ResponseEntity
. created ( linkTo ( methodOn ( ProductController . class )
. findOne ( savedProduct . getId ())). toUri ())
. body ( productAssembler . toModel (savedProduct));
}
The API returns a 201 Created status with a Location header pointing to the newly created resource, following REST best practices.
Dependency Injection
Spring Boot uses constructor-based dependency injection throughout the application:
public class UserController {
private final UserRepository userRepository ;
private final UserAssembler assembler ;
private final PasswordEncoder passwordEncoder ;
public UserController (
UserRepository userRepository ,
UserAssembler assembler ,
PasswordEncoder passwordEncoder
) {
this . userRepository = userRepository;
this . assembler = assembler;
this . passwordEncoder = passwordEncoder;
}
}
Benefits:
Immutable dependencies (final fields)
Clear required dependencies
Easier testing with mock objects
No need for @Autowired annotation
Error Handling
The API uses custom exception classes and @ControllerAdvice for centralized error handling:
// Custom exception
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException ( Long id ) {
super ( "Could not find product " + id);
}
}
// Global exception handler
@ ControllerAdvice
class ProductNotFoundAdvice {
@ ResponseBody
@ ExceptionHandler ( ProductNotFoundException . class )
@ ResponseStatus ( HttpStatus . NOT_FOUND )
String productNotFoundHandler ( ProductNotFoundException ex ) {
return ex . getMessage ();
}
}
Key Design Principles
Each business domain (product, user, cart, category) is organized into its own package with all related components. This makes the codebase easier to navigate and maintain.
Each class has a single, well-defined purpose:
Controllers handle HTTP
Repositories handle data access
Entities represent database tables
Assemblers transform entities to API resources
Controllers depend on repository interfaces, not concrete implementations. Spring Data JPA generates the implementation at runtime.
Next Steps
Data Models Explore the entity relationships and database schema
HATEOAS Support Learn how hypermedia links enhance the API