Introduction
This E-Commerce Backend API is built following Clean Architecture (also known as Hexagonal Architecture) principles combined with the CQRS (Command Query Responsibility Segregation) pattern. This architectural approach ensures:
Separation of concerns - Business logic is independent of frameworks
Testability - Core logic can be tested without external dependencies
Maintainability - Clear boundaries make the codebase easier to understand and modify
Flexibility - Easy to swap implementations (e.g., change database or framework)
Architectural Layers
The application is organized into three main layers, each with distinct responsibilities:
Domain Layer Pure business logic and rules
Application Layer Use cases and orchestration
Infrastructure Layer External concerns (DB, API, frameworks)
Layer Dependency Flow
Infrastructure → Application → Domain
Dependencies point inward: Infrastructure depends on Application, Application depends on Domain, but Domain depends on nothing. This is the key principle of Clean Architecture.
Directory Structure
The codebase is organized by feature modules , each containing all three architectural layers:
com.example.demo/
├── DemoApplication.java # Main entry point
├── core/ # Shared infrastructure
│ └── infrastructures/
│ └── exceptionhandler/
│ └── GlobalExceptionHandler.java # Global error handling
│
├── usuario/ # User module
│ ├── domain/ # Business logic
│ │ ├── models/
│ │ │ └── UsuarioPOJO.java # Domain model
│ │ ├── repositories/
│ │ │ └── UsuarioRepository.java # Repository interface
│ │ ├── vo/
│ │ │ └── ContrasenaVO.java # Value Object
│ │ └── exceptions/
│ │ └── InvalidPasswordException.java
│ │
│ ├── application/ # Use cases
│ │ ├── commands/
│ │ │ ├── RegistrarUsuarioCommand.java
│ │ │ └── handlers/
│ │ │ └── RegistrarUsuarioHandler.java
│ │ └── querys/
│ │ ├── LoginUsuarioQuery.java
│ │ └── handlers/
│ │ └── LoginUsuarioHandler.java
│ │
│ └── infrastructure/ # External concerns
│ ├── controllers/
│ │ ├── UsuarioRestController.java
│ │ └── RegistrarUsuarioRequest.java
│ ├── entities/
│ │ └── UsuarioEntity.java # JPA entity
│ ├── repositories/
│ │ ├── UsuarioJPARepository.java
│ │ └── UsuarioAdapterRepository.java
│ └── config/
│ └── CorsConfig.java
│
└── producto/ # Product module
├── domain/
├── application/
└── infrastructure/
Domain Layer
The Domain Layer is the heart of the application, containing pure business logic with no external dependencies.
Domain Models (POJOs)
Domain models represent business entities and contain business rules:
@ AllArgsConstructor
@ NoArgsConstructor
@ Getter
@ Setter
public class UsuarioPOJO {
private Long id ;
private String usuario ;
private ContrasenaVO contrasena ;
}
Models use the POJO (Plain Old Java Object) suffix to distinguish them from JPA entities. They contain only business logic, no persistence annotations.
Value Objects (VOs)
Value Objects encapsulate domain concepts and enforce business rules:
@ Getter
public class ContrasenaVO {
private final String value ;
public ContrasenaVO ( String value ) {
if (value == null || value . isEmpty ()) {
throw new InvalidPasswordException ( "La contraseña no puede estar vacía" );
}
if ( value . length () < 8 ) {
throw new InvalidPasswordException ( "La contraseña debe tener al menos 8 caracteres" );
}
if ( value . length () > 64 ) {
throw new InvalidPasswordException ( "La contraseña no puede superar los 64 caracteres" );
}
if ( ! value . matches ( ".*[A-Za-z].*" ) || ! value . matches ( ".* \\ d.*" )) {
throw new InvalidPasswordException ( "La contraseña debe contener al menos una letra y un número" );
}
this . value = value;
}
@ Override
public String toString () {
return "****" ; // Never expose password in logs
}
}
Key characteristics of Value Objects:
Immutable (final fields, no setters)
Self-validating (validation in constructor)
Represent domain concepts (not just primitive types)
Define equality based on value, not identity
Repository Interfaces
Domain defines repository interfaces, but doesn’t implement them:
public interface UsuarioRepository {
UsuarioPOJO findUsuarioByUsuarioYContrasena ( String usuario , String contrasena );
UsuarioPOJO saveUsuario ( String usuario , String contrasena );
}
The Domain Layer must NEVER import classes from Application or Infrastructure layers. Dependencies flow inward only.
Application Layer
The Application Layer orchestrates business workflows using CQRS pattern to separate read and write operations.
CQRS Pattern
CQRS (Command Query Responsibility Segregation) separates operations into two categories:
Commands Mutate state - Create, Update, Delete operations
Queries Read state - Fetch data without side effects
Commands
Commands represent state-changing operations:
@ Setter
@ Getter
@ AllArgsConstructor
public class RegistrarUsuarioCommand {
private String usuario ;
private ContrasenaVO contrasena ;
}
Command Handlers
Handlers execute commands by coordinating domain objects:
@ Service
public class RegistrarUsuarioHandler {
private final UsuarioRepository usuarioRepository ;
public RegistrarUsuarioHandler ( UsuarioRepository usuarioRepository ) {
this . usuarioRepository = usuarioRepository;
}
public UsuarioPOJO handle ( RegistrarUsuarioCommand command ) {
return usuarioRepository . saveUsuario (
command . getUsuario (),
command . getContrasena (). getValue ()
);
}
}
Queries
Queries represent read operations:
@ Getter
@ Setter
@ AllArgsConstructor
public class LoginUsuarioQuery {
private String usuario ;
private String contrasena ;
}
Query Handlers
Query handlers fetch data without modifying state:
@ Service
public class LoginUsuarioHandler {
private final UsuarioRepository usuarioRepository ;
public LoginUsuarioHandler ( UsuarioRepository usuarioRepository ) {
this . usuarioRepository = usuarioRepository;
}
public UsuarioPOJO handle ( LoginUsuarioQuery query ) {
return usuarioRepository . findUsuarioByUsuarioYContrasena (
query . getUsuario (),
query . getContrasena ()
);
}
}
Use Cases
For simpler operations, the application layer may use traditional Use Case classes:
@ Service
public class RecuperarTodosLosProductosUC {
private final ProductoRepository productoRepository ;
public RecuperarTodosLosProductosUC ( ProductoRepository productoRepository ) {
this . productoRepository = productoRepository;
}
public List < ProductoPOJO > execute () {
return productoRepository . findAll ();
}
}
Infrastructure Layer
The Infrastructure Layer contains framework-specific code and external integrations.
REST Controllers
Controllers handle HTTP requests and delegate to application layer:
@ RestController
public class UsuarioRestController {
private final RegistrarUsuarioHandler registrarUsuarioHandler ;
private final LoginUsuarioHandler loginUsuarioHandler ;
public UsuarioRestController (
RegistrarUsuarioHandler registrarUsuarioHandler ,
LoginUsuarioHandler loginUsuarioHandler
) {
this . registrarUsuarioHandler = registrarUsuarioHandler;
this . loginUsuarioHandler = loginUsuarioHandler;
}
@ GetMapping ( "/login/{usuario}/{contrasena}" )
public UsuarioPOJO loginUsuario (
@ PathVariable String usuario ,
@ PathVariable String contrasena
) {
LoginUsuarioQuery query = new LoginUsuarioQuery (usuario, contrasena);
return loginUsuarioHandler . handle (query);
}
@ PostMapping ( "/registro" )
public UsuarioPOJO registroUsuario (@ RequestBody RegistrarUsuarioRequest request ) {
RegistrarUsuarioCommand command = new RegistrarUsuarioCommand (
request . usuario (),
new ContrasenaVO ( request . contrasena ())
);
return registrarUsuarioHandler . handle (command);
}
}
Request/Response DTOs
DTOs (Data Transfer Objects) handle external data formats:
public record RegistrarUsuarioRequest (
String usuario,
String contrasena
) {}
Java records are perfect for DTOs - immutable, concise, and automatically implement equals/hashCode.
JPA Entities
Entities handle database persistence:
@ Entity
@ Table ( name = "Usuario" )
@ Getter
@ Setter
@ NoArgsConstructor
@ AllArgsConstructor
public class UsuarioEntity {
@ Id
@ GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long idUsuario ;
@ Column ( name = "usuario" )
private String usuario ;
@ Column ( name = "contrasena" )
private String contrasena ;
public UsuarioEntity ( String usuario , String contrasena ) {
this . usuario = usuario;
this . contrasena = contrasena;
}
// Convert to domain model
public UsuarioPOJO toDomain () {
return new UsuarioPOJO (
this . idUsuario ,
this . usuario ,
new ContrasenaVO ( this . contrasena )
);
}
// Convert from domain model
public static UsuarioEntity fromDomain ( UsuarioPOJO usuario ) {
return new UsuarioEntity (
usuario . getId (),
usuario . getUsuario (),
usuario . getContrasena (). getValue ()
);
}
}
Key points:
Entities have JPA annotations (@Entity, @Table, @Id)
Conversion methods (toDomain(), fromDomain()) bridge entity and domain models
Keeps persistence concerns separate from business logic
Repository Adapters
Adapters implement domain repository interfaces using JPA:
@ Repository
public class UsuarioAdapterRepository implements UsuarioRepository {
private final UsuarioJPARepository usuarioJPARepository ;
public UsuarioAdapterRepository ( UsuarioJPARepository usuarioJPARepository ) {
this . usuarioJPARepository = usuarioJPARepository;
}
@ Override
public UsuarioPOJO findUsuarioByUsuarioYContrasena ( String usuario , String contrasena ) {
return usuarioJPARepository
. findByUsuarioAndContrasena (usuario, contrasena)
. toDomain ();
}
@ Override
public UsuarioPOJO saveUsuario ( String usuario , String contrasena ) {
UsuarioEntity usuarioEntity = new UsuarioEntity (usuario, contrasena);
return usuarioJPARepository . save (usuarioEntity). toDomain ();
}
}
This follows the Adapter Pattern , allowing the domain to remain independent of JPA.
Global Exception Handling
Centralized error handling for consistent API responses:
@ ControllerAdvice
public class GlobalExceptionHandler {
@ ExceptionHandler ( InvalidPasswordException . class )
public ResponseEntity < String > handleInvalidPasswordException (
InvalidPasswordException ex
) {
return ResponseEntity
. status ( HttpStatus . BAD_REQUEST )
. body ( ex . getMessage ());
}
}
Dependency Injection
The application uses Spring’s dependency injection throughout:
// Constructor injection (recommended)
public RegistrarUsuarioHandler ( UsuarioRepository usuarioRepository) {
this . usuarioRepository = usuarioRepository;
}
Spring automatically wires dependencies thanks to @Service, @Repository, and @RestController annotations. No @Autowired needed with constructor injection.
Benefits of This Architecture
Independent of Frameworks Core logic doesn’t depend on Spring, JPA, or any framework
Testable Business logic can be tested without databases or web servers
Independent of UI Same API can serve web, mobile, or other clients
Independent of Database Can swap MySQL for PostgreSQL, MongoDB, etc. with minimal changes
Clear Boundaries Each layer has explicit responsibilities
Scalable CQRS allows independent scaling of read and write operations
Request Flow Example
Let’s trace a user registration request through all layers:
HTTP Request Arrives
Client sends POST to /registro with JSON body
Controller Layer (Infrastructure)
UsuarioRestController receives request, creates RegistrarUsuarioCommand
Command Handler (Application)
RegistrarUsuarioHandler receives command, orchestrates business logic
Value Object Validation (Domain)
ContrasenaVO constructor validates password rules
Repository Interface (Domain)
Handler calls usuarioRepository.saveUsuario()
Repository Adapter (Infrastructure)
UsuarioAdapterRepository implements the interface using JPA
JPA Entity (Infrastructure)
UsuarioEntity persists to MySQL database
Response Mapping
Entity converted to domain model (toDomain()), returned as JSON
Best Practices
DO’s
Keep domain models pure (no framework annotations)
Use Value Objects for domain concepts
Validate in Value Object constructors
Use constructor injection
Separate commands from queries
Convert entities to domain models at boundaries
DON’Ts
Don’t put business logic in controllers
Don’t let domain depend on infrastructure
Don’t mix JPA entities with domain models
Don’t skip validation in Value Objects
Don’t expose entities directly via API
Further Reading
CQRS Pattern Learn about command and query separation
User API Endpoints Explore user registration and login endpoints
Value Objects Understand domain validation with value objects
Error Handling Global exception handling patterns