Estructura de Paquetes
Nuestros servicios backend siguen una estructura de Paquete por Funcionalidad organizada en cuatro módulos hermanos. Este documento es la fuente de verdad sobre dónde debe ir el nuevo código.
Nota sobre el lenguaje. Los ejemplos a continuación están escritos en Java/Spring Boot porque es nuestro stack backend principal, pero la estructura en sí misma es independiente del lenguaje y obligatoria en todos los lenguajes backend que usamos (Kotlin, Go, Python, Node, etc.). Traduzca las extensiones de archivo y las anotaciones del framework; mantenga la estructura de carpetas, los sufijos de nomenclatura (
*Db,*Repo,*RepoImpl,*Dao,*Controller,*ControllerImpl) y la división interfaz/implementación sin cambios.
Principio fundamental
Agrupe por funcionalidad, no por capa técnica. Un cambio en la funcionalidad "pedidos" debe tocar archivos que vivan uno al lado del otro, no archivos dispersos en los paquetes controllers/, services/, repositories/. La división por capas técnicas ocurre en el límite del módulo (db, client, capabilities, starter), y el paquete-por-funcionalidad ocurre dentro de cada módulo.
Los cuatro módulos
Cada servicio está compuesto por estos cuatro módulos hermanos:
<service>/
├── capabilities/ # funcionalidad/lógica de negocio — el cerebro
├── db/ # persistencia: entidades, repositorios, migraciones
├── client/ # integraciones salientes con sistemas externos
└── starter/ # Spring Boot starter — conecta todo y se ejecuta
capabilities/ — lógica de negocio
Cada funcionalidad obtiene su propio paquete, y dentro de cada funcionalidad hay un sub-paquete por endpoint de API (getById, create, updateById, getByQuery, …). Cada sub-funcionalidad posee tres carpetas hermanas:
service/— la única clase de servicio para esa APIdto/—request/yresponse/(lo que aplique)mapper/— traduce entidades*Db↔ DTOs (ambas direcciones) solo para esta sub-funcionalidad
El controlador de la funcionalidad vive en la raíz del paquete de la funcionalidad, dividido en una interfaz y una implementación, al igual que los repositorios:
capabilities/<feature>/<Feature>Controller.java— interfaz; declara rutas, verbos HTTP, tipos de request/response, anotaciones de documentación de APIcapabilities/<feature>/<Feature>ControllerImpl.java— implementación; lleva la anotación del framework (@RestController) y delega cada ruta al servicio de sub-funcionalidad correspondiente
Compartido en todas las sub-funcionalidades de esa funcionalidad. Ver Controladores.
- Depende de:
db(solo a través de interfaces*Repo— ver Regla de uso de DB),client - Nunca depende de:
starter - No contiene: clase
main, ninguna@SpringBootApplication - Posee: controladores HTTP, DTOs de request/response de API, mappers, servicios
db/ — persistencia
Organizado una carpeta por tabla. Cada carpeta de tabla tiene exactamente tres sub-carpetas:
db/<tableName>/
├── model/
│ ├── <EntityName>Db.java # entidad JPA principal
│ └── <Other>Db.java # sub-modelos (tipos embebidos, entidades hijas, proyecciones)
├── dao/
│ └── <EntityName>Dao.java # acceso a datos de bajo nivel (consultas Criteria/JPQL/nativas)
└── repository/
├── <EntityName>Repo.java # interfaz PÚBLICA — el único tipo que ve capabilities
└── impl/
└── <EntityName>RepoImpl.java # compone Spring Data + Dao detrás de la interfaz
Convención de nomenclatura
| Capa | Sufijo | Ejemplo |
|---|---|---|
| Entidad JPA | Db | TestcaseDb |
| Interfaz de repositorio | Repo | TestcaseRepo |
| Implementación de repositorio | RepoImpl | TestcaseRepoImpl |
| Helper de acceso a datos | Dao | TestcaseDao |
Regla de uso de DB
Capabilities debe depender solo de interfaces *Repo. *RepoImpl y *Dao son internos — nunca se inyectan o referencian desde fuera del paquete db/<tableName>/. Esto es lo que mantiene la capa de persistencia intercambiable y las pruebas de capabilities enfocadas en el contrato.
- Depende de: nada en este servicio
- Expone: interfaces
*Repoy tipos de modelo*Db - Nunca contiene: lógica de negocio, transformación a DTOs de API
client/ — integraciones salientes
Contiene el código que llama a otros servicios de los que este servicio depende. Principalmente una sub-carpeta por microservicio dependiente en nuestra propia plataforma (paymentService, userService, executionService, …). Los envoltorios de SDK de terceros o productores de mensajes también pueden vivir aquí, pero el patrón dominante — y la razón por la que existe este módulo — son los clientes de microservicios dependientes.
client/<microserviceName>/
├── <Microservice>Client.java # interfaz — de lo que depende capabilities
├── impl/
│ └── <Microservice>ClientImpl.java # implementación: llamada HTTP/gRPC/SDK, reintentos, timeouts
├── dto/ # formas de request/response para la API de ESTE microservicio
│ ├── request/
│ └── response/
└── mapper/ # opcional: traduce entre sus formas y las nuestras
Reglas
-
Una sub-carpeta por microservicio dependiente, nombrada según el servicio. No agrupe múltiples microservicios en un solo paquete, incluso si están "relacionados".
-
Capabilities depende solo de interfaces
*Client.*ClientImples interno — misma regla que*Repo/*RepoImpl. -
La interfaz del cliente expone la API del microservicio en términos de sus propios DTOs; capabilities puede adaptarlos más si es necesario.
-
Las preocupaciones transversales (reintentos, circuit breaking, registro de request/response, cabeceras de autenticación) pertenecen a
impl/— capabilities nunca debe verlas. -
Depende de: nada en este servicio
-
Expone: interfaces
*Clienty sus DTOs de request/response -
Nunca contiene: lógica de negocio, orquestación de capabilities
starter/ — raíz de composición
El único módulo que se ejecuta. Contiene @SpringBootApplication, manejadores globales de excepciones REST, configuración de seguridad, filtros y application.yml. Sin controladores — esos viven en capabilities/.
- Depende de:
capabilities(y transitivamentedb,client) - Posee: el artefacto desplegable, configuración web transversal (seguridad, CORS, manejo de errores)
- Nunca posee: controladores de funcionalidad o DTOs de funcionalidad
Regla de dependencias
starter → capabilities → { db, client }
Las flechas van en una dirección. db y client nunca importan de capabilities o starter. capabilities nunca importa de starter.
Controladores
Hay un controlador por funcionalidad, no por sub-funcionalidad, y vive en la raíz del paquete de la funcionalidad dentro de capabilities/. Está dividido en dos archivos, reflejando la división *Repo / *RepoImpl:
capabilities/<feature>/<Feature>Controller.java— interfaz. Declara las rutas, verbos HTTP, parámetros de path/query, tipos de DTO de request/response y anotaciones de documentación de API (OpenAPI/Swagger). Cualquiera que lea la interfaz debe ver todo el contrato HTTP.capabilities/<feature>/<Feature>ControllerImpl.java— implementación. Lleva la anotación del framework (@RestControlleren Spring), es el objetivo de inyección para los servicios de sub-funcionalidad y contiene un método corto por ruta que llama alservice/correspondiente y devuelve el resultado. Sin lógica más allá de la delegación.
¿Por qué la división? La interfaz es el contrato de API — estable, documentado, comprobable de forma aislada. La implementación es cableado — puede cambiar sin cambiar el contrato. Misma razón que *Repo vs *RepoImpl.
El controlador delega cada ruta HTTP al servicio de sub-funcionalidad correspondiente en capabilities/<feature>/<sub-feature>/service/. Los DTOs de request/response son propiedad de la sub-funcionalidad (capabilities/<feature>/<sub-feature>/dto/); el controlador es solo un adaptador HTTP ligero.
¿Por qué dentro de capabilities/ y no en starter/? El controlador, su servicio y sus DTOs cambian juntos cuando una API cambia — mantenerlos en un solo módulo preserva la promesa de paquete-por-funcionalidad de "una funcionalidad, un lugar". starter/ está reservado para cableado transversal (seguridad, manejadores de errores, punto de entrada de la aplicación), no para código web por funcionalidad.
Ejemplo práctico: una funcionalidad testcase
El árbol a continuación usa extensiones .java como ilustración. Sustituya la extensión de archivo y los modismos del framework de su lenguaje — Kotlin .kt, paquetes Go, módulos Python, etc. — pero mantenga la estructura de carpetas y los sufijos de nomenclatura idénticos.
testcase-service/
├── capabilities/
│ └── com/example/testcase/capabilities/
│ └── testcase/ # funcionalidad principal
│ ├── TestcaseController.java # interfaz — contrato HTTP, declaraciones de rutas, docs de API
│ ├── TestcaseControllerImpl.java # implementación — @RestController, delega a servicios de sub-funcionalidad
│ ├── getById/ # sub-funcionalidad = una API
│ │ ├── service/
│ │ │ └── GetTestcaseByIdService.java
│ │ ├── dto/
│ │ │ └── response/
│ │ │ └── GetTestcaseByIdResponse.java
│ │ └── mapper/
│ │ └── GetTestcaseByIdMapper.java # TestcaseDb → respuesta
│ ├── create/
│ │ ├── service/
│ │ │ └── CreateTestcaseService.java
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── CreateTestcaseRequest.java
│ │ │ └── response/
│ │ │ └── CreateTestcaseResponse.java
│ │ └── mapper/
│ │ └── CreateTestcaseMapper.java # request → TestcaseDb, TestcaseDb → respuesta
│ ├── updateById/
│ │ ├── service/
│ │ │ └── UpdateTestcaseByIdService.java
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── UpdateTestcaseByIdRequest.java
│ │ │ └── response/
│ │ │ └── UpdateTestcaseByIdResponse.java
│ │ └── mapper/
│ │ └── UpdateTestcaseByIdMapper.java
│ └── getByQuery/
│ ├── service/
│ │ └── GetTestcaseByQueryService.java
│ ├── dto/
│ │ ├── request/
│ │ │ └── GetTestcaseByQueryRequest.java
│ │ └── response/
│ │ └── GetTestcaseByQueryResponse.java
│ └── mapper/
│ └── GetTestcaseByQueryMapper.java
│
├── db/ # una carpeta por tabla
│ └── com/example/testcase/db/
│ └── testcase/ # nombre de tabla
│ ├── model/
│ │ ├── TestcaseDb.java # entidad JPA — principal
│ │ └── TestcaseStepDb.java # sub-modelo (hijo/embebido)
│ ├── dao/
│ │ └── TestcaseDao.java # helper de consultas/criteria, uso de paquete-privado
│ └── repository/
│ ├── TestcaseRepo.java # interfaz — capabilities depende de ESTO
│ └── impl/
│ └── TestcaseRepoImpl.java # compone Spring Data + TestcaseDao
│
├── client/ # una carpeta por microservicio dependiente
│ └── com/example/testcase/client/
│ ├── executionService/ # microservicio dependiente al que llamamos
│ │ ├── ExecutionServiceClient.java # interfaz
│ │ ├── impl/
│ │ │ └── ExecutionServiceClientImpl.java # llamada HTTP/gRPC, reintentos, timeouts
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── RunTestcaseRequest.java
│ │ │ └── response/
│ │ │ └── RunTestcaseResponse.java
│ │ └── mapper/
│ │ └── ExecutionServiceMapper.java
│ └── userService/ # otro microservicio dependiente
│ ├── UserServiceClient.java
│ └── impl/
│ └── UserServiceClientImpl.java
│
└── starter/ # solo cableado transversal — sin controladores
└── com/example/testcase/
├── TestcaseServiceApplication.java
├── config/
│ ├── SecurityConfig.java
│ └── WebConfig.java
├── error/
│ └── GlobalExceptionHandler.java # @ControllerAdvice — transversal, no por funcionalidad
└── resources/application.yml
Reglas prácticas
- ¿Añadir una nueva API a una funcionalidad existente? Cree un nuevo paquete de sub-funcionalidad dentro de
capabilities/<feature>/con su propioservice/,dto/{request,response}/ymapper/, luego agregue la declaración de la ruta a<Feature>Controller.java(interfaz) y el método delegante a<Feature>ControllerImpl.java. No cree un segundo controlador. - ¿Añadir una funcionalidad completamente nueva? Cree el paquete de la funcionalidad en
capabilities/(controlador en la raíz, sub-funcionalidades por API) y la carpeta de tabla correspondiente endb/<tableName>/conmodel/,dao/,repository/{impl/}. Añada un cliente solo si llama a un sistema externo. No toquestarter/. - Siempre inyecte
*Repo, nunca*RepoImplo*Dao. Capabilities depende solo de la interfaz.*RepoImply*Daoson internos dedb/<tableName>/— existen para permitirle cambiar la estrategia de consulta o el almacenamiento subyacente sin cambiar el código de capabilities. - Siempre inyecte
*Client, nunca*ClientImpl. Misma regla para llamadas a microservicios dependientes. Capabilities depende de la interfaz; el transporte (HTTP, gRPC, SDK) vive detrás deimpl/. - Una carpeta por microservicio dependiente dentro de
client/. Nómbrela según el microservicio. No agrupe "todos los servicios internos" en un solo paquete. - La granularidad de sub-funcionalidad se detiene en
capabilities/.db/se organiza por tabla;client/por sistema externo. Dividir cualquiera de ellos por API dispersaría el código cohesivo. - Los DTOs de request/response y su mapper son por sub-funcionalidad, nunca compartidos. Si
createyupdateByIdse parecen hoy, mantenga dos DTOs y dos mappers. Compartir acopla dos APIs que divergirán independientemente. - Los mappers convierten entidad ↔ DTO directamente. Sin capa de modelo de dominio intermedio.
<SubFeature>Mapperconvierte*Db↔ los DTOs de request/response de esa sub-funcionalidad. - ¿Tres sub-funcionalidades tienen código similar? Resista la tentación de extraer un servicio o mapper compartido hasta que tenga el mismo patrón en cuatro o más lugares.
- ¿Reutilización entre funcionalidades? Ponga las utilidades compartidas en un paquete
common/dentro decapabilities/si son de lógica de negocio, o enclient/si son de comunicación. No cree un módulo separado para ello. - ¿Añadir un nuevo microservicio dependiente? Cree una carpeta en
client/<microserviceName>/con*Client.java(interfaz),impl/,dto/{request,response}/y opcionalmentemapper/. Siga el mismo patrón de división interfaz/implementación. - Nunca importe un tipo de
starter/desdecapabilities/.starter/es el consumidor final;capabilities/no sabe nada sobre cómo se despliega, asegura o ejecuta la aplicación. - Los manejadores globales de excepciones, filtros y configuraciones de seguridad pertenecen a
starter/. Si un manejador de errores se vuelve específico de una funcionalidad, muévalo al paquete de esa funcionalidad encapabilities/. Pero la línea base —@ControllerAdviceque capturaException— se queda enstarter/. - Un solo
application.ymlenstarter/. No distribuya archivos de propiedades entre módulos. Use perfiles de Spring (application-{profile}.yml) para diferencias entre entornos, pero todos viven enstarter/resources/. - Las migraciones de base de datos (Flyway/Liquibase) se colocan en
db/src/main/resources/db/migration/. No enstarter/. El módulodbposee el esquema y las migraciones;starter/solo las ejecuta. - La modularidad es un firewall, no un dogma. Si una clase de
capabilities/realmente necesita un bean de configuración definido enstarter/(por ejemplo, unRestTemplateconfigurado), exponga una interfaz encapabilities/e implemente la configuración enstarter/. No importe el bean de configuración directamente. - Los tests unitarios viven en el mismo módulo que el código que prueban. Los tests de
*Repoestán endb/src/test/, los tests de*Serviceestán encapabilities/src/test/, los tests de integración que arrancan toda la aplicación están enstarter/src/test/.