Overview
The ecommerce API uses JPA (Java Persistence API) entities to represent the database schema. Entities define the structure of data, relationships between tables, and validation rules.
All entities are located in domain-specific packages: product, user, category, and cart.
Entity Relationships
The database schema includes six main entities with various relationships:
Core Entities
Product
Represents items available for purchase in the ecommerce store.
Entity Definition
Field Details
Relationships
@ Entity
public class Product {
@ Id
@ GeneratedValue
private Long id ;
private String name ;
private String description ;
private BigDecimal price ;
private Long stockQuantity ;
private BigDecimal discount ;
@ ManyToOne
@ JoinColumn ( name = "user_id" )
private User user ;
@ OneToMany ( mappedBy = "product" ,
cascade = CascadeType . ALL ,
orphanRemoval = true )
private List < ProductCategory > categories = new ArrayList <>();
@ CreatedDate
@ Column ( nullable = false , updatable = false )
private LocalDateTime createdAt ;
@ UpdateTimestamp
@ Column ( nullable = false , updatable = true )
private LocalDateTime updatedAt ;
}
Field Type Description Constraints idLongPrimary key Auto-generated nameStringProduct name Required descriptionStringProduct details Optional priceBigDecimalProduct price Required stockQuantityLongAvailable inventory Required discountBigDecimalDiscount percentage Optional userUserProduct creator Foreign key to User categoriesList<ProductCategory>Product categories Cascade delete createdAtLocalDateTimeCreation timestamp Auto-set, immutable updatedAtLocalDateTimeLast update timestamp Auto-updated
Many-to-One with User @ ManyToOne
@ JoinColumn ( name = "user_id" )
private User user ;
Each product is created by one user. A user can create many products. One-to-Many with ProductCategory @ OneToMany ( mappedBy = "product" ,
cascade = CascadeType . ALL ,
orphanRemoval = true )
private List < ProductCategory > categories = new ArrayList <>();
A product can belong to multiple categories through the join entity.
The @UpdateTimestamp annotation automatically updates the updatedAt field whenever the entity is modified.
User
Represents registered users (customers and admins) in the system.
Entity Definition
Field Details
Role Enum
@ Entity
@ Table ( name = "users" )
public class User {
@ Id
@ GeneratedValue
private Long id ;
private String name ;
private String email ;
private String password ;
private Role role ;
@ CreatedDate
@ Column ( nullable = false , updatable = false )
private LocalDateTime createdAt ;
}
Field Type Description Constraints idLongPrimary key Auto-generated nameStringUser’s full name Required emailStringUser’s email Required, unique passwordStringEncrypted password Required, stored hashed roleRoleUser role (CUSTOMER/ADMIN) Required createdAtLocalDateTimeRegistration timestamp Auto-set, immutable
public enum Role {
CUSTOMER ,
ADMIN
}
CUSTOMER : Regular user who can browse and purchase productsADMIN : Privileged user with management capabilities
The @Table(name = "users") annotation maps this entity to the users table, as user is a reserved keyword in many databases.
Category
Represents product categories for organization and filtering.
@ Entity
public class Category {
@ Id
@ GeneratedValue
private Long id ;
@ Column ( unique = true )
private String name ;
@ OneToMany ( mappedBy = "category" )
@ JsonIgnore
private List < ProductCategory > products = new ArrayList <>();
}
Field Type Description Constraints idLongPrimary key Auto-generated nameStringCategory name Required, unique productsList<ProductCategory>Products in category Ignored in JSON
Key Points:
Category names must be unique (@Column(unique = true))
The @JsonIgnore prevents circular references in API responses
Products are accessed through the ProductCategory join entity
Cart
Represents a user’s shopping cart with automatic total calculation.
Entity Definition
Automatic Calculation
Validation
@ Entity
@ Table ( name = "cart" )
@ Data
@ NoArgsConstructor
@ Validated
public class Cart {
@ Id
@ GeneratedValue
private Long id ;
@ ManyToOne
@ JoinColumn ( name = "customer_id" )
private User user ;
@ OneToMany ( fetch = FetchType . LAZY , cascade = CascadeType . ALL )
private List < CartItems > cartItems = new ArrayList <>();
@ Column ( name = "total" , nullable = false )
@ Min ( value = 0 , message = "Total must be greater than or equal to 0" )
private BigDecimal total ;
@ Column ( name = "discount" )
@ Min ( value = 0 , message = "Discount cannot be negative" )
private BigDecimal discount ;
@ CreatedDate
@ Column ( updatable = false )
private LocalDateTime createdDate ;
@ LastModifiedDate
@ Column ( name = "updatedAt" )
private LocalDateTime lastModifiedDate ;
@ PrePersist
@ PreUpdate
private void onPersistorUpdate () {
calculateTotalAndDiscount ();
this . lastModifiedDate = LocalDateTime . now ();
}
}
void calculateTotalAndDiscount () {
setTotal ( BigDecimal . ZERO );
setDiscount ( BigDecimal . ZERO );
if (( this . cartItems != null ) && ( ! this . cartItems . isEmpty ())) {
for ( CartItems cartItem : this . cartItems ) {
BigDecimal total = cartItem . getProduct (). getPrice ()
. multiply ( new BigDecimal ( cartItem . getQuantity ()));
setTotal ( getTotal (). add (total));
BigDecimal itemDiscount = total
. multiply ( cartItem . getProduct (). getDiscount ())
. divide ( BigDecimal . valueOf ( 100 ), 2 , RoundingMode . HALF_UP );
setDiscount ( getDiscount (). add (itemDiscount));
}
}
}
Calculation Logic:
Calculate subtotal: price × quantity for each item
Calculate discount: subtotal × (discount% / 100)
Sum all subtotals and discounts
Round discounts to 2 decimal places
Field Validation:
@Min(value = 0) on total prevents negative totals
@Min(value = 0) on discount prevents negative discounts
@Validated enables bean validation
Lifecycle Hooks:
@PrePersist: Runs before initial save
@PreUpdate: Runs before every update
Both trigger calculateTotalAndDiscount() automatically
The Cart entity uses Lombok annotations (@Data, @NoArgsConstructor) to reduce boilerplate code for getters, setters, and constructors.
CartItems
Represents individual items in a shopping cart (join entity between Cart and Product).
@ Entity
@ Table ( name = "cart_items" )
@ Data
@ NoArgsConstructor
@ Validated
public class CartItems {
@ Id
@ GeneratedValue ( strategy = GenerationType . SEQUENCE )
private Long id ;
@ ManyToOne ( fetch = FetchType . LAZY , optional = false )
@ JoinColumn ( name = "cart_id" , nullable = false )
private Cart cart ;
@ ManyToOne ( fetch = FetchType . LAZY , optional = false )
@ JoinColumn ( name = "product_id" , nullable = false )
private Product product ;
@ Column ( name = "quantity" , nullable = false )
@ Min ( value = 0 , message = "Product quantity cannot be negative" )
private int quantity ;
@ PrePersist
@ PreUpdate
public void updateTotalAndDiscount () {
this . getCart (). calculateTotalAndDiscount ();
}
}
Field Type Description Constraints idLongPrimary key Auto-generated (SEQUENCE) cartCartParent cart Required, lazy loaded productProductProduct reference Required, lazy loaded quantityintNumber of items Required, ≥ 0
Key Features:
Lazy loading (FetchType.LAZY) for performance
optional = false ensures cart and product are required
Lifecycle hooks trigger parent cart recalculation
Sequence-based ID generation
ProductCategory
Join entity representing the many-to-many relationship between Products and Categories.
@ Entity
public class ProductCategory {
@ Id
@ GeneratedValue
private Long id ;
@ ManyToOne ( optional = false )
@ JsonIgnore
private Product product ;
@ ManyToOne ( optional = false )
private Category category ;
}
Field Type Description Constraints idLongPrimary key Auto-generated productProductProduct reference Required, JSON ignored categoryCategoryCategory reference Required
Why a Join Entity?
Allows many-to-many relationship between Product and Category
Can be extended with additional fields (e.g., sort order, featured status)
Provides explicit control over the relationship
Avoids @ManyToMany annotation complexity
Common Patterns
Timestamps
All main entities track creation and modification times:
@ CreatedDate
@ Column ( nullable = false , updatable = false )
private LocalDateTime createdAt ;
@ UpdateTimestamp // or @LastModifiedDate
private LocalDateTime updatedAt ;
@CreatedDate and @UpdateTimestamp are automatically managed by Spring Data JPA and Hibernate.
Cascade Operations
Cascade types determine how operations propagate to related entities:
// Cascade all operations (persist, merge, remove, refresh, detach)
@ OneToMany ( mappedBy = "product" ,
cascade = CascadeType . ALL ,
orphanRemoval = true )
private List < ProductCategory > categories ;
orphanRemoval = true : Deletes ProductCategory entities when removed from the Product’s list.
Lazy vs Eager Loading
Lazy Loading (Default)
Eager Loading
@ ManyToOne ( fetch = FetchType . LAZY )
private Product product ;
When to use:
Large related entities
Relationship not always needed
Performance is critical
Benefit : Related entity loaded only when accessed@ ManyToOne ( fetch = FetchType . EAGER )
private User user ;
When to use:
Small related entities
Relationship always needed
Avoiding N+1 query problems
Drawback : Always loads related entity (potential performance hit)
Validation Annotations
The entities use Bean Validation (JSR-380) annotations:
Annotation Purpose Example @MinMinimum numeric value @Min(value = 0)@Column(nullable = false)Database NOT NULL constraint Field required in DB @Column(unique = true)Unique constraint email, category name@ValidatedEnable class-level validation On entity class
Combine JPA constraints (@Column) with validation annotations (@Min) for both database-level and application-level validation.
Best Practices
private BigDecimal price ; // ✓ Correct
private double price ; // ✗ Avoid
Why : BigDecimal provides precise decimal arithmetic without floating-point errors, essential for financial calculations.
Implement equals() and hashCode()
@ Override
public boolean equals ( Object o) {
if ( ! (o instanceof Product product)) return false ;
return Objects . equals (id, product . id ) &&
Objects . equals (name, product . name );
}
@ Override
public int hashCode () {
return Objects . hash (id, name, description, price);
}
Why : Required for proper collection handling and Hibernate session management.
Avoid Bidirectional @JsonIgnore Issues
Use @JsonIgnore on one side of bidirectional relationships to prevent infinite recursion: // In ProductCategory:
@ ManyToOne
@ JsonIgnore // Prevents circular reference
private Product product ;
Next Steps
Architecture Learn about the overall system architecture
HATEOAS Understand how entities are transformed to API resources