OperacionInventario es un microservicio REST para procesar operaciones de inventario.
El flujo principal recibe una solicitud, transforma los datos de entrada al modelo de dominio, genera el folio, consolida los detalles por SKU y tipo de movimiento, consulta en bloque el catálogo, separa los detalles catalogados de los no catalogados, genera los identificadores de detalle y persiste en Firebird la cabecera y únicamente los detalles catalogados.
La solución sigue principios de Arquitectura Hexagonal (Ports & Adapters), manteniendo el dominio independiente de la tecnología de persistencia y de los detalles de infraestructura.
- API REST con Spring Boot.
- Java 21.
- Arquitectura Hexagonal / Ports & Adapters.
- Separación entre dominio, aplicación e infraestructura.
- Persistencia mediante
JdbcTemplate. - Base de datos Firebird.
- Driver JDBC Jaybird 6.0.6.
- Pool de conexiones HikariCP.
- Generación de folios desde Java.
- Consulta del último folio mediante un puerto de persistencia.
- Consolidación de SKU por
SKU + Tipo, antes de consultar el catálogo. - Consulta masiva del catálogo por los SKU únicos consolidados.
- Asignación de
Id_Productodesde el catálogo; el cliente ya no envía este identificador. - Clasificación entre
DetalleDoc(catalogados) yDetalleNoCatalogado(no encontrados). - Persistencia exclusiva de los detalles catalogados.
- Generación de
ID_DET. - Inserción de una cabecera y múltiples detalles.
- Transacciones mediante
@Transactional. - Rollback automático ante errores.
- Puerto
EventoPublicadordefinido para desacoplar la futura publicación de eventos. - La integración concreta del adaptador Kafka y su publicación posterior al commit queda como siguiente etapa.
- Manejo centralizado de excepciones REST.
- Respuestas de error controladas sin exponer stack traces.
- Logging para diagnóstico interno.
| Tecnología | Uso |
|---|---|
| Java 21 | Lenguaje |
| Spring Boot | Framework principal |
| Spring Web | API REST |
| Spring JDBC | Acceso mediante JdbcTemplate |
| JdbcTemplate | Ejecución de SQL |
| HikariCP | Pool de conexiones |
| Firebird | Base de datos |
| Jaybird 6.0.6 | Driver JDBC |
| Kafka | Integración de mensajería prevista; adaptador pendiente |
| Maven | Gestión de dependencias |
Dependencias relevantes:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.firebirdsql.jdbc</groupId>
<artifactId>jaybird</artifactId>
<version>6.0.6</version>
</dependency>
spring-boot-starter-data-jdbcno se utiliza. El proyecto trabaja directamente conJdbcTemplate.
Este diagrama forma parte del diseño arquitectónico que define el Líder Técnico y proporciona a los programadores como guía de implementación. Su propósito es establecer claramente las capas, responsabilidades, puertos y adaptadores que deben respetarse durante el desarrollo.
+---------------------+
| CLIENTE REST |
+----------+----------+
|
v
+---------------------+
| INFRAESTRUCTURA |
| |
| Controlador |
| Transformador |
| Exception Handler |
+----------+----------+
|
v
+---------------------+
| APLICACIÓN |
| |
| ProcesarExistencia |
| @Transactional |
+----------+----------+
|
+----------+----------+
| |
v v
+-----------------+ +-----------------+
| DOMINIO | | PUERTOS |
| | | |
| Existencia | | ExistenciaRepo |
| Detalle | | FolioRepo |
| ConsolidadorSKU | +--------+--------+
| GeneradorIdDet | |
+-----------------+ v
+---------------------+
| ADAPTADORES |
| |
| ExistenciaRepoBD |
| ConsultaFolioRepoBD |
+----------+----------+
|
v
+---------------------+
| FIREBIRD |
| |
| INV_O_EXISTENCIAS |
| INV_O_EXIS_DET |
+---------------------+
Campos utilizados:
ID_EMPLEADO
ID_SUCURSAL
FOLIO
FECHA_DOC
Campos utilizados:
ID_UN_MED
ID_SKU_CODIGO
ID_PRODUCTO
FECHA_DOC
FOLIO
ID_DET
TIPO
CANTIDAD
Relación conceptual:
INV_O_EXISTENCIAS
1
|
| N
v
INV_O_EXIS_DET
public class Existencia implements Serializable {
private static final long serialVersionUID = 7605612050451854434L;
private String Id_Empleado;
private Integer Id_Sucursal;
private LocalDateTime Fecha_Doc;
private String Folio;
private List<DetalleExistencia> DetalleDoc;
}public class DetalleExistencia implements Serializable {
private static final long serialVersionUID = -5808974323605733728L;
private Integer Id_Un_Medida;
private String Id_SKU_Codigo;
private Integer Id_Producto;
private LocalDateTime FechaDoc;
private String Folio;
private Integer Id_Det;
private Integer Tipo;
private BigDecimal Cantidad;
}flowchart TD
A[Cliente REST] --> B[ExistenciaControlador]
B --> C[ExistenciaTransformador]
C --> D[Existencia]
D --> E[ProcesarExistenciaServicio]
E --> F[GeneradorFolio]
F --> G[ConsultaFolioRepositorio]
G --> H[ConsultaFolioRepositorioBD]
H --> I[(Firebird)]
E --> J[ConsolidadorSKU: SKU + Tipo]
J --> K[ClasificarCatalogoServicio]
K --> L[ConsultaProductoRepositorio]
L --> M[ConsultaProductoRepositorioBD]
M --> I
K --> N[Catalogados / No catalogados]
N --> O[GeneradorIdDetalle]
O --> P[ExistenciaRepositorio]
P --> Q[ExistenciaRepositorioBD]
Q --> I
Q --> R[Commit transaccional]
R -. siguiente etapa .-> S[EventoPublicador / Kafka]
com.ope.inv
│
├── dominio
│ ├── ...
│ ├── modelo
│ │ ├── ClaveSKU.java
│ │ ├── Existencia.java
│ │ ├── DetalleExistencia.java
│ │ └── ObjetoNoCatalogado.java
│ ├── servicio
│ │ ├── ConsolidadorSKU.java
│ │ ├── GeneradorFolio.java
│ │ └── GeneradorIdDetalle.java
│ │
│ ├── puertos
│ │ ├── ent
│ │ │ └── ProcesarExistencia
│ │ └── sal
│ │ ├── ConsultaFolioRepositorio
│ │ ├── ConsultaProductoRepositorio
│ │ ├── EventoPublicador
│ │ └── ExistenciaRepositorio
│ │
│ └── excepciones
│ └── DatoInvalidoException.java
│
├── aplicacion
│ └── servicio
│ ├── ProcesarExistenciaServicio.java
│ └── ClasificarCatalogoServicio.java
│
└── infraestructura
├── entrada
│ └── ...
│
├── persistencia
│ ├── ConsultaFolioRepositorioBD.java
│ ├── ConsultaProductoRepositorioBD.java
│ └── ExistenciaRepositorioBD.java
│
└── excepciones
├── ErrorRespuesta.java
└── ManejadorExcepciones.java
Contiene conceptos y reglas propias del negocio:
ExistenciaDetalleExistenciaConsolidadorSKUGeneradorIdDetalleDatoInvalidoException
El dominio no debe depender de Firebird, JDBC, HTTP ni Kafka.
ProcesarExistenciaServicio coordina el caso de uso:
Recibir existencia
↓
Generar folio
↓
Generar folio
↓
Consolidar por SKU + Tipo
↓
Consultar catálogo en bloque
↓
Separar catalogados y no catalogados
↓
Generar ID_DET para detalles catalogados
↓
Guardar cabecera y solo detalles catalogados
↓
Commit
↓
Publicación Kafka (integración pendiente)
La transacción se controla mediante @Transactional.
Contiene los detalles tecnológicos:
- Controladores REST.
- Transformadores de entrada.
- JdbcTemplate.
- Firebird.
- Implementaciones de repositorios.
- Manejo HTTP.
- Configuración.
- Kafka.
flowchart LR
CLIENTE[Cliente REST]
KAFKA[Kafka]
DB[(Firebird)]
subgraph INFRA[Infraestructura / Adaptadores]
CONTROLLER[ExistenciaControlador]
TRANSFORMADOR[ExistenciaTransformador]
BD[ExistenciaRepositorioBD]
FOLIOBD[ConsultaFolioRepositorioBD]
HANDLER[ManejadorExcepciones]
end
subgraph APP[Aplicación]
SERVICIO[ProcesarExistenciaServicio]
end
subgraph DOM[Dominio]
MODELO[Existencia / DetalleExistencia]
SERVICIOS[ConsolidadorSKU / GeneradorIdDetalle]
PORT1[ExistenciaRepositorio]
PORT2[ConsultaFolioRepositorio]
EX[DatoInvalidoException]
end
CLIENTE --> CONTROLLER
CONTROLLER --> TRANSFORMADOR
TRANSFORMADOR --> MODELO
CONTROLLER --> SERVICIO
SERVICIO --> SERVICIOS
SERVICIO --> PORT1
SERVICIO --> PORT2
BD -. implementa .-> PORT1
FOLIOBD -. implementa .-> PORT2
BD --> DB
FOLIOBD --> DB
SERVICIO -. integración pendiente .-> KAFKA
EX --> HANDLER
HANDLER --> CLIENTE
El servicio depende del puerto, no directamente de Firebird:
ProcesarExistenciaServicio
|
v
ExistenciaRepositorio <-- interfaz
^
|
ExistenciaRepositorioBD <-- adapter
|
v
Firebird
Por eso la base de datos puede cambiar sin modificar las reglas de negocio.
Puerto de salida:
public interface ExistenciaRepositorio {
void guardar(Existencia existencia);
}La aplicación expresa:
Necesito guardar una existencia.
No necesita conocer cómo se ejecuta el SQL.
Adaptador de infraestructura:
@Repository
public class ExistenciaRepositorioBD
implements ExistenciaRepositorio {
// JdbcTemplate
// INSERT cabecera
// INSERT detalles
}Este adapter conoce SQL, JdbcTemplate, Firebird, tablas y columnas.
Otro puerto de salida que permite consultar el último folio sin acoplar el dominio a Firebird.
Su implementación concreta es:
ConsultaFolioRepositorioBD
La operación de guardado realiza:
Existencia
|
+-- INSERT INV_O_EXISTENCIAS
|
+-- INSERT INV_O_EXIS_DET
+-- Detalle 1
+-- Detalle 2
+-- Detalle 3
+-- Detalle N
Cada detalle recibe la información necesaria de la cabecera, incluyendo:
Folio
FechaDoc
La operación de negocio está protegida mediante @Transactional.
flowchart TD
A[Inicio @Transactional] --> B[INSERT Cabecera]
B --> C[INSERT Detalle 1]
C --> D[INSERT Detalle 2]
D --> E[INSERT Detalle N]
E --> F{¿Todo correcto?}
F -->|Sí| G[COMMIT]
F -->|No| H[ROLLBACK]
G --> I[Disponible para publicación posterior]
H --> J[Fin con error; no publicar]
Si un detalle falla:
Cabecera ✓
Detalle 1 ✓
Detalle 2 ✗
↓
ROLLBACK
↓
No queda información parcial
El código de persistencia no realiza manualmente commit() ni rollback(); Spring administra la transacción.
El folio se genera desde Java:
GeneradorFolio
↓
ConsultaFolioRepositorio
↓
ConsultaFolioRepositorioBD
↓
Firebird
↓
Último folio
↓
Calcular consecutivo
↓
Generar nuevo folio
La implementación concreta de base de datos permanece en infraestructura.
Antes de persistir los detalles se realiza la consolidación.
Ejemplo:
Entrada:
SKU A → 10
SKU B → 5
SKU A → 7
SKU C → 3
↓
Resultado:
SKU A → 17
SKU B → 5
SKU C → 3
Principio utilizado durante el desarrollo:
"Si ya lo contabilicé (lo vi), ¿por qué tendría que volver a calcularlo si ya lo tengo?"
Las estructuras de búsqueda como Map pueden utilizarse cuando permiten evitar recorridos repetidos.
Después de consolidar, ClasificarCatalogoServicio obtiene la lista de SKU únicos y consulta el puerto ConsultaProductoRepositorio una sola vez. El adaptador ConsultaProductoRepositorioBD implementa la consulta en Firebird contra INV_C_OBJETOS y devuelve la relación SKU → ID_PRODUCTO.
El campo Id_Producto no se recibe en la petición de entrada. Se completa internamente a partir del catálogo para cada detalle encontrado.
El resultado se separa en dos colecciones:
DetalleDoc: contiene solamente los detalles cuyo SKU se encontró en catálogo. Estos son los únicos detalles que se insertan enINV_O_EXIS_DET.DetalleNoCatalogado: contiene los SKU no encontrados y sus cantidades consolidadas. Se conservan en la respuesta para su seguimiento, pero no se insertan como detalles porque no tienen la referencia de catálogo requerida.
La cabecera de la existencia se registra con su folio. Si todos los SKU fueran no catalogados, la regla actual permite que la cabecera no tenga detalles insertados; este caso debe seguir considerándose en las pruebas funcionales.
Si la entrada contiene:
| SKU | Cantidades recibidas | Estado en catálogo |
|---|---|---|
SKU-5002 |
1 + 1 + 1 | Catalogado |
SKU-4003 |
1 | Catalogado |
SKU-6003 |
1 | Catalogado |
SKU-88 |
2 | No catalogado |
SKU-010 |
4 + 11 | No catalogado |
El resultado esperado es:
DetalleDoc:SKU-5002con cantidad 3,SKU-4003con cantidad 1 ySKU-6003con cantidad 1.DetalleNoCatalogado:SKU-88con cantidad 2 ySKU-010con cantidad 15.
Los SKU no catalogados no se vuelven a consolidar después de la clasificación: la consolidación inicial ya agrupó los duplicados por SKU + Tipo.
JSON de entrada (sin Id_Producto)
↓
Consolidar por SKU + Tipo
↓
Consulta masiva de SKU únicos
↓
┌──────┴──────┐
↓ ↓
Catalogado No catalogado
↓ ↓
Completar DetalleNoCatalogado
Id_Producto (con cantidad consolidada)
↓ ↓
DetalleDoc No insertar en INV_O_EXIS_DET
↓
Insertar en INV_O_EXIS_DET
- Los SKU repetidos con el mismo tipo se consolidan sumando sus cantidades.
- Un SKU catalogado se conserva como detalle independiente de otros SKU, aunque compartan el mismo
Id_Producto. - Los SKU no encontrados se excluyen de
DetalleDocy se incluyen enDetalleNoCatalogado. - En el escenario probado con ocho registros, el resultado fue de tres detalles catalogados (
SKU-5002= 3,SKU-4003= 1 ySKU-6003= 1) y dos no catalogados (SKU-88= 2 ySKU-010= 15). - La verificación del JSON confirma la clasificación y consolidación. La persistencia debe verificarse consultando la tabla de detalle para confirmar que los no catalogados no se insertaron.
Paquete:
com.ope.inv.dominio.excepciones
Clase:
DatoInvalidoException
Ejemplo:
throw new DatoInvalidoException(
"Tipo de movimiento no válido: " + tipo
);Esta excepción no conoce HTTP.
Paquete:
com.ope.inv.infraestructura.excepciones
Clases:
ErrorRespuesta
ManejadorExcepciones
ManejadorExcepciones utiliza:
@RestControllerAdvicepara centralizar las excepciones REST.
Cuando Jackson no puede convertir un dato:
HttpMessageNotReadableException
La API devuelve:
{
"codigo": "JSON_INVALIDO",
"mensaje": "El formato de los datos enviados no es válido"
}HTTP:
400 Bad Request
Cuando una regla propia detecta un dato incorrecto:
{
"codigo": "DATO_INVALIDO",
"mensaje": "Tipo de movimiento no válido: -"
}HTTP:
400 Bad Request
El stack trace no forma parte de la respuesta pública de la API.
Error
|
+--------+--------+
| |
v v
Cliente Logs
| |
JSON controlado Stack trace
El consumidor recibe información controlada.
Los logs internos conservan información técnica para diagnóstico.
spring.datasource.driver-class-name=org.firebirdsql.jdbc.FBDriver
spring.datasource.url=jdbc:firebird://localhost:3050/C:/Aplicativos/BDFirebird/OPERACIONGENERAL.FDB
spring.datasource.username=USROPE
spring.datasource.password=Ejecutor
spring.datasource.hikari.connection-test-query=SELECT 1 FROM RDB$DATABASE
spring.sql.init.mode=neverConfiguración principal:
logging.level.root=INFO
logging.level.com.ope.inv=DEBUG- Esto permite observar el flujo del microservicio durante desarrollo y diagnóstico.
- Adicional crea mediante el archivo xml de configuracion 2 archivos, uno
app-info.logdonde registra los sucesos y otroapp-error.logdonde registra errores de excepcion que suceda en el MicroServicio. - Una configuracion en los logs es la rotativa que con llevan, despues de 10 MB de capaciadad crea otro archivo con el siguiente formato
dd-MM-yyyy.
El cambio de Oracle a Firebird no requirió modificar las reglas de negocio. La adaptación se realizó en infraestructura.
Se utiliza SQL directo mediante JdbcTemplate para controlar explícitamente las consultas e inserciones.
La generación del folio, consolidación y reglas de negocio permanecen en Java.
Se utiliza @Transactional para asegurar atomicidad.
El puerto EventoPublicador ya está definido para mantener la mensajería desacoplada. En la versión documentada, el adaptador concreto Kafka y la llamada de publicación posterior al commit aún deben integrarse. Cuando se implemente, debe garantizarse que un fallo de transacción no publique un evento de éxito.
Las excepciones del negocio no conocen HTTP. La infraestructura transforma esas excepciones en respuestas REST.
Los principios SOLID sirven como criterio de diseño para mantener OperacionInventario desacoplado, mantenible y fácil de extender. En este proyecto no se aplican como teoría aislada: se reflejan principalmente en la separación entre dominio, aplicación e infraestructura, en el uso de puertos y adaptadores y en la responsabilidad específica de cada componente.
Cada clase debe tener una razón principal para cambiar. En el proyecto se refleja en ExistenciaTransformador (transformación), ProcesarExistenciaServicio (caso de uso), GeneradorFolio (folio), ConsolidadorSKU (regla de consolidación), GeneradorIdDetalle (identificadores), ExistenciaRepositorioBD (persistencia) y ManejadorExcepciones (respuesta HTTP). Esto evita clases que concentren transformación, negocio, persistencia y HTTP.
Los componentes deben poder extenderse sin modificar innecesariamente el código existente. ProcesarExistenciaServicio depende de ExistenciaRepositorio, no de ExistenciaRepositorioBD; por ello se puede incorporar otro adaptador de persistencia sin cambiar el caso de uso.
Una implementación de un puerto debe poder sustituirse por otra respetando su contrato. Un repositorio Firebird y una futura implementación para Oracle pueden sustituirse desde el punto de vista de ProcesarExistenciaServicio, siempre que cumplan el contrato de ExistenciaRepositorio.
Las interfaces deben ser pequeñas y enfocadas. ConsultaFolioRepositorio y ExistenciaRepositorio representan capacidades concretas, evitando una interfaz enorme con operaciones no relacionadas.
Los componentes de alto nivel dependen de abstracciones y no de detalles tecnológicos. ProcesarExistenciaServicio depende de ExistenciaRepositorio, mientras ExistenciaRepositorioBD conoce JdbcTemplate y Firebird. GeneradorFolio depende de ConsultaFolioRepositorio, mientras ConsultaFolioRepositorioBD contiene el detalle de acceso a la base de datos.
La migración de Oracle a Firebird demuestra el valor del desacoplamiento: el dominio y la aplicación permanecen independientes de la base de datos y el cambio se concentra principalmente en infraestructura y configuración.
DOMINIO
|
APLICACIÓN
|
PUERTOS
|
+---------+---------+
| |
Adaptador Oracle Adaptador Firebird
| |
Oracle JdbcTemplate
|
Firebird
SOLID no significa crear interfaces o clases por obligación. En este proyecto debe utilizarse para mantener responsabilidades claras, dependencias invertidas, puertos pequeños y adaptadores sustituibles, evitando abstracciones innecesarias. En conjunto, SOLID refuerza la arquitectura hexagonal y ayuda a que las reglas de negocio permanezcan independientes de HTTP, Firebird, JdbcTemplate, controladores y otros detalles externos.
Al cierre de esta etapa:
- La aplicación inicia correctamente.
- Spring Boot levanta el servicio REST.
- Firebird está configurado.
- HikariCP establece conexiones.
JdbcTemplateejecuta consultas e inserciones.- Se obtiene el último folio desde Firebird.
- Se genera el nuevo folio.
- Se consolidan los SKU por SKU + tipo de movimiento.
- Se consulta el catálogo en bloque después de consolidar.
Id_Productose obtiene del catálogo, no de la petición de entrada.- Se separan los detalles catalogados de
DetalleNoCatalogado. - Solo los detalles catalogados se insertan en
INV_O_EXIS_DET. - Se generan los identificadores de detalle para los detalles que se procesan como catalogados.
- Se inserta la cabecera y los detalles válidos.
- La operación está protegida por transacción.
- Se implementó manejo global de excepciones.
- Los errores REST tienen un formato controlado.
- El stack trace no se expone al consumidor.
- Existe el puerto
EventoPublicador; falta completar el adaptador Kafka y verificar la publicación posterior al commit.
OperacionInventario queda estructurado para que las reglas de negocio estén separadas de los mecanismos técnicos.
La idea central de la arquitectura es:
El dominio define qué necesita la aplicación; los puertos definen cómo se comunica con el exterior; los adaptadores implementan esas comunicaciones.
Esto permite mantener la lógica de negocio estable aunque cambien tecnologías como la base de datos, el mecanismo de mensajería o componentes de infraestructura.