Skip to main content

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.
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-Repository Pattern

The API follows the Controller-Repository pattern where controllers handle HTTP requests and delegate data operations to repositories.
@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:
@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;
}
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

MethodEndpointPurposeResponse
GET/productsList all products200 OK with PagedModel
GET/products/{id}Get single product200 OK with EntityModel
POST/productsCreate product201 Created with Location header
PUT/PATCH/products/{id}Update product200 OK
DELETE/products/{id}Delete product204 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

Build docs developers (and LLMs) love