Visión General de la Arquitectura

iMirly sigue la Arquitectura Hexagonal (también conocida como Puertos y Adaptadores), un patrón de diseño que desacopla completamente la lógica de negocio de las dependencias externas como bases de datos, frameworks y UI. Esto asegura que el dominio permanezca puro, testable y agnóstico al framework.

¿Por qué Arquitectura Hexagonal?

Las arquitecturas tradicionales en capas acoplan fuertemente la lógica de negocio a las preocupaciones de infraestructura. La arquitectura hexagonal invierte estas dependencias:

Capas Tradicionales

La lógica de negocio depende de las implementaciones de base de datos y framework. Difícil de probar, difícil de cambiar.

Hexagonal (Puertos & Adaptadores)

La lógica de negocio define interfaces (puertos). La infraestructura las implementa (adaptadores). Fácil de probar, intercambiar implementaciones.

Principios Core

  1. 1

    Independencia del Dominio

    La lógica de negocio tiene cero dependencias en Spring, JPA o cualquier biblioteca externa

  2. 2

    Inversión de Control

    El dominio define interfaces (puertos), la infraestructura las implementa

  3. 3

    Testabilidad

    La lógica pura de dominio puede probarse sin bases de datos ni frameworks

  4. 4

    Flexibilidad

    Intercambia base de datos, framework o UI sin tocar las reglas de negocio

Arquitectura de Alto Nivel

┌───────────────────────────────────────────────────────────┐
│                     Android Frontend                      │
│            (Kotlin + Jetpack Compose)                     │
│                                                           │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                 │
│  │ UI Views │→ │ViewModels│→ │ Retrofit │                 │
│  └──────────┘  └──────────┘  └─────┬────┘                 │
└────────────────────────────────────┼──────────────────────┘
                                      │ HTTP/JSON
                                      ↓
┌────────────────────────────────────────────────────────────┐
│              Spring Boot Backend (Java 21)                 │
│                                                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │           INFRASTRUCTURE LAYER                      │   │
│  │  (Adapters - Input & Output)                        │   │
│  │                                                     │   │
│  │  INPUT (Driving)        OUTPUT (Driven)             │   │
│  │  ┌──────────────┐      ┌──────────────┐             │   │
│  │  │ REST         │      │ JPA          │             │   │
│  │  │ Controllers  │      │ Repositories │             │   │
│  │  │              │      │              │             │   │
│  │  │ JWT Filters  │      │ Security     │             │   │
│  │  └──────┬───────┘      └──────▲───────┘             │   │
│  └─────────┼────────────────────┼──────────────────────┘   │
│            │                    │                          │
│            ↓ uses InPorts       │ implements OutPorts      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │         APPLICATION LAYER                           │   │
│  │  (Use Cases / Services)                             │   │
│  │                                                     │   │
│  │  ┌──────────────────┐  ┌──────────────────┐         │   │
│  │  │ RegisterUser     │  │ CreateAnuncio    │         │   │
│  │  │ Service          │  │ Service          │         │   │
│  │  │                  │  │                  │         │   │
│  │  │ implements       │  │ implements       │         │   │
│  │  │ RegisterUserUse  │  │ ManageAnuncio    │         │   │
│  │  │ Case (InPort)    │  │ UseCase (InPort) │         │   │
│  │  └────────┬─────────┘  └────────┬─────────┘         │   │
│  └───────────┼──────────────────────┼──────────────────┘   │
│              │ uses Domain          │                      │
│              ↓                      ↓                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │            DOMAIN LAYER                             │   │
│  │  (Pure Business Logic - No Dependencies)            │   │
│  │                                                     │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐           │   │
│  │  │   User   │  │ Anuncio  │  │ Solicitud│           │   │
│  │  │          │  │ Servicio │  │ Contrato │           │   │
│  │  │ - saldo  │  │          │  │          │           │   │
│  │  │ + restar │  │ + pausar │  │ + aceptar│           │   │
│  │  │   Saldo()│  │ Anuncio()│  │()        │           │   │
│  │  └──────────┘  └──────────┘  └──────────┘           │   │
│  │                                                     │   │
│  │  ┌─────────────────────────────────────────┐        │   │
│  │  │ PORTS (Interfaces)                      │        │   │
│  │  │                                         │        │   │
│  │  │ InPorts          OutPorts              │         │   │
│  │  │ (Driven by UI)   (Drive Infrastructure)│         │   │
│  │  └─────────────────────────────────────────┘        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                            │
└──────────────────────────┬─────────────────────────────────┘
                           │
                           ↓
                  ┌─────────────────┐
                  │   PostgreSQL    │
                  │   Database      │
                  └─────────────────┘
            
            

Estructura de Módulos

El backend está organizado en cuatro contextos acotados (módulos), cada uno siguiendo la arquitectura hexagonal:

bash
backend/src/main/java/tfg/imirly/
├── auth/          # Autenticación de usuarios y gestión de billetera
├── catalog/       # Listados de servicios y categorías
├── contracting/   # Ciclo de vida de contratos y pagos
└── messages/      # Chat en tiempo real y propuestas

Anatomía del Módulo

Cada módulo sigue la misma estructura de tres capas:

bash
module/
├── domain/              # CAPA INTERNA - Lógica pura de negocio
│   ├── model/          # Entidades de dominio (User, AnuncioServicio, etc.)
│   └── port/
│       └── out/        # OutPorts: Interfaces que necesita el dominio
├── application/         # CAPA MEDIA - Casos de uso
│   ├── port/
│   │   └── in/         # InPorts: Interfaces de casos de uso
│   └── service/        # Implementaciones de servicios (ejecutan lógica)
└── infrastructure/      # CAPA EXTERNA - Adaptadores
    ├── input/          # Adaptadores driving (REST controllers, filtros)
    │   └── rest/
    │       ├── Controller.java
    │       └── dto/    # DTOs Request/Response
    └── output/         # Adaptadores driven (JPA repos, APIs externas)
        └── persistence/
            ├── entity/        # Entidades JPA
            ├── repository/    # Repositorios Spring Data
            └── RepositoryAdapter.java  # Implementa OutPorts del dominio

💡 Insight Clave: Las dependencias siempre apuntan hacia adentro. Infraestructura depende de Aplicación, Aplicación depende de Dominio. Dominio NO depende de nada.

Módulos del Sistema

Auth

Autenticación de usuarios, gestión de perfiles y billetera virtual

JWT Wallet Ratings

Catalog

Gestión de categorías, subcategorías y anuncios de servicios

Categorías Búsqueda Favoritos

Contracting

Ciclo de vida de contratos, propuestas de presupuesto y pagos

Contratos Escrow Tareas

Messages

Chat en tiempo real con propuestas integradas y notificaciones

WebSocket Chat Propuestas
Capa 1

Capa de Dominio (Lógica de Negocio Core)

El corazón de la aplicación. Contiene:

Modelos de Dominio

Clases Java puras con lógica de negocio y cero dependencias de framework:

java
// auth/domain/model/User.java
package tfg.imirly.auth.domain.model;

import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;

@Getter
@Setter
public class User {
    private UUID id;
    private String nombre;
    private String email;
    private String passwordHash;
    private UserType tipo;  // CLIENTE or PROFESIONAL
    private String documentoIdentidad;
    private boolean verificado;
    private LocalDateTime fechaRegistro;
    
    // Billetera virtual
    private Double saldo;
    private Double puntuacionTotal;
    private Integer numeroValoraciones;
    
    // Métodos de lógica de negocio
    public void restarSaldo(Double cantidad) {
        if (cantidad == null || cantidad <= 0) {
            throw new IllegalArgumentException("La cantidad debe ser mayor que cero.");
        }
        this.saldo -= cantidad;
    }
    
    public void sumarSaldo(Double cantidad) {
        if (cantidad == null || cantidad <= 0) {
            throw new IllegalArgumentException("La cantidad debe ser mayor que cero.");
        }
        this.saldo += cantidad;
    }
    
    public Double getValoracionMedia() {
        if (numeroValoraciones == null || numeroValoraciones == 0)
            return 0.0;
        return Math.round((puntuacionTotal / numeroValoraciones) * 10.0) / 10.0;
    }
}

Nota: Sin @Entity, sin @Table, sin anotaciones Spring. Esto es lógica pura de negocio.

OutPorts (Interfaces Definidas por el Dominio)

El dominio declara lo que necesita del mundo exterior:

java
// auth/domain/port/out/UserRepositoryPort.java
package tfg.imirly.auth.domain.port.out;

import tfg.imirly.auth.domain.model.User;
import java.util.Optional;
import java.util.UUID;

public interface UserRepositoryPort {
    User save(User user);
    Optional<User> findByEmail(String email);
    Optional<User> findById(UUID id);
    boolean existsByEmail(String email);
}

// auth/domain/port/out/PasswordEncoderPort.java
public interface PasswordEncoderPort {
    String encode(String rawPassword);
    boolean matches(String rawPassword, String encodedPassword);
}

Estas son interfaces, no implementaciones. El dominio no le importa si usas BCrypt, Argon2 o MD5 (por favor no). Solo necesita codificación de contraseñas.

Capa 2

Capa de Aplicación (Casos de Uso)

Orquesta la lógica de dominio para cumplir casos de uso específicos.

InPorts (Interfaces de Casos de Uso)

Define lo que la aplicación puede hacer:

java
// auth/application/port/in/RegisterUserUseCase.java
package tfg.imirly.auth.application.port.in;

import tfg.imirly.auth.domain.model.User;

public interface RegisterUserUseCase {
    User registerUser(RegisterUserCommand command);
}

// auth/application/port/in/RegisterUserCommand.java
public class RegisterUserCommand {
    private final String nombre;
    private final String email;
    private final String rawPassword;
    private final UserType tipo;
    private final String documentoIdentidad;
    
    // Constructor, getters
}

Servicios (Implementaciones de Casos de Uso)

Ejecutan lógica de negocio coordinando objetos de dominio y puertos:

java
// auth/application/service/RegisterUserService.java
package tfg.imirly.auth.application.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import tfg.imirly.auth.application.port.in.RegisterUserCommand;
import tfg.imirly.auth.application.port.in.RegisterUserUseCase;
import tfg.imirly.auth.domain.model.User;
import tfg.imirly.auth.domain.port.out.PasswordEncoderPort;
import tfg.imirly.auth.domain.port.out.UserRepositoryPort;

@Service
@RequiredArgsConstructor
public class RegisterUserService implements RegisterUserUseCase {
    
    private final UserRepositoryPort userRepository;
    private final PasswordEncoderPort passwordEncoder;
    
    @Override
    public User registerUser(RegisterUserCommand command) {
        // Regla de negocio: email debe ser único
        if (userRepository.existsByEmail(command.getEmail())) {
            throw new IllegalArgumentException("Email ya registrado");
        }
        
        // Hashear la contraseña
        String hashedPassword = passwordEncoder.encode(command.getRawPassword());
        
        // Crear objeto de dominio
        User newUser = new User(
            command.getNombre(),
            command.getEmail(),
            hashedPassword,
            command.getTipo(),
            command.getDocumentoIdentidad()
        );
        
        // Persistir vía puerto
        return userRepository.save(newUser);
    }
}

El servicio depende de puertos (UserRepositoryPort, PasswordEncoderPort), no de implementaciones concretas. La inyección de dependencias de Spring proporciona los adaptadores en runtime.

Capa 3

Capa de Infraestructura (Adaptadores)

Conecta el dominio con el mundo real.

Adaptadores de Entrada (Driving - Controladores REST)

Reciben peticiones del mundo exterior e invocan casos de uso:

java
// auth/infrastructure/input/rest/AuthController.java
package tfg.imirly.auth.infrastructure.input.rest;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import tfg.imirly.auth.application.port.in.*;
import tfg.imirly.auth.domain.model.User;
import tfg.imirly.auth.domain.model.UserType;
import tfg.imirly.auth.infrastructure.input.rest.dto.*;

@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
    
    private final RegisterUserUseCase registerUserUseCase;
    private final LoginUserUseCase loginUserUseCase;
    
    @PostMapping("/register")
    public ResponseEntity<?> registerUser(@RequestBody RegisterUserRequest request) {
        try {
            // Mapear DTO a Command
            RegisterUserCommand command = new RegisterUserCommand(
                request.getNombre(),
                request.getEmail(),
                request.getRawPassword(),
                UserType.valueOf(request.getTipoUsuario().toUpperCase()),
                request.getDocumentoIdentidad()
            );
            
            // Ejecutar caso de uso
            User newUser = registerUserUseCase.registerUser(command);
            
            return ResponseEntity.status(HttpStatus.CREATED)
                .body("Usuario registrado con ID: " + newUser.getId());
        } catch (IllegalArgumentException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(e.getMessage());
        }
    }
}

Adaptadores de Salida (Driven - Repositorios JPA)

Implementan OutPorts del dominio usando Spring Data JPA:

java
// auth/infrastructure/output/persistence/UserRepositoryAdapter.java
package tfg.imirly.auth.infrastructure.output.persistence;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import tfg.imirly.auth.domain.model.User;
import tfg.imirly.auth.domain.port.out.UserRepositoryPort;
import tfg.imirly.auth.infrastructure.output.persistence.entity.UserEntity;
import tfg.imirly.auth.infrastructure.output.persistence.repository.SpringDataUserRepository;

import java.util.Optional;
import java.util.UUID;

@Component
@RequiredArgsConstructor
public class UserRepositoryAdapter implements UserRepositoryPort {
    
    private final SpringDataUserRepository springDataRepo;
    private final UserMapper mapper;
    
    @Override
    public User save(User user) {
        UserEntity entity = mapper.toEntity(user);
        UserEntity saved = springDataRepo.save(entity);
        return mapper.toDomain(saved);
    }
    
    @Override
    public Optional<User> findByEmail(String email) {
        return springDataRepo.findByEmail(email)
            .map(mapper::toDomain);
    }
    
    @Override
    public boolean existsByEmail(String email) {
        return springDataRepo.existsByEmail(email);
    }
}

El adaptador traduce entre modelos de dominio y entidades JPA:

java
// auth/infrastructure/output/persistence/entity/UserEntity.java
@Entity
@Table(name = "usuarios")
@Getter
@Setter
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    
    @Column(nullable = false)
    private String nombre;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    @Column(nullable = false)
    private String passwordHash;
    
    @Enumerated(EnumType.STRING)
    private UserType tipo;
    
    private String documentoIdentidad;
    private boolean verificado;
    private LocalDateTime fechaRegistro;
    private Double saldo;
    private Double puntuacionTotal;
    private Integer numeroValoraciones;
}

Ejemplo de Flujo de Datos: Registro de Usuario

1

App Android Envía Petición

La app construye un RegisterUserRequest y lo envía vía Retrofit

2

Controlador REST Recibe (Infraestructura Input)

AuthController mapea el DTO a RegisterUserCommand y llama a RegisterUserUseCase

3

Caso de Uso Ejecuta (Capa de Aplicación)

RegisterUserService valida unicidad del email vía UserRepositoryPort, hashea contraseña vía PasswordEncoderPort

4

Objeto de Dominio Creado

Se crea un User puro con reglas de negocio aplicadas (saldo = 0.0, verificado = false)

5

Persistencia vía Adaptador (Infraestructura Output)

UserRepositoryAdapter convierte User a UserEntity y guarda en PostgreSQL

6

Respuesta Fluye de Vuelta

El User guardado se devuelve a través de las capas, el controlador envía HTTP 201 con el ID del usuario

Beneficios de Esta Arquitectura

Testabilidad

Prueba la lógica de negocio sin bases de datos, frameworks o dependencias externas

Flexibilidad

Cambia PostgreSQL por MongoDB, Spring por Quarkus, REST por GraphQL - sin tocar las reglas de negocio

Mantenibilidad

Separación clara de responsabilidades. Sabes exactamente dónde añadir features o corregir bugs

Enfoque en Dominio

La lógica de negocio es explícita, no dispersa entre controladores y repositorios