Version: 1.0
Fecha: 16 de Marzo, 2026
Estado: NORMATIVO - Estructura Obligatoria
Arquitecto: Carlos Alberto Torres Camargo
Servicio de referencia: nebula-accounting-core
Cada microservicio Nebula sigue una estructura estandar generada por el SimappeArchetype y refinada segun las necesidades del dominio. La organizacion combina:
v1/ (agrupacion por contexto de negocio)Patron obligatorio: Controller → Service → Component → Repository
Referencia: Patron Backend
nebula-accounting-core/
├── src/
│ ├── main/
│ │ ├── java/com/centrica/nebula/accounting/core/
│ │ │ │
│ │ │ ├── config/ # [TRANSVERSAL] Configuraciones Spring
│ │ │ │ ├── AppConfig.java # Beans basicos: TimeZone, PasswordEncoder
│ │ │ │ ├── AsyncConfig.java # @EnableAsync, @EnableScheduling, pools de hilos
│ │ │ │ ├── ClientConfig.java # WebClient.Builder con propagacion de token
│ │ │ │ ├── DockerMongoHealthConfig.java # Health check MongoDB para Docker
│ │ │ │ ├── KafkaAdapterConfig.java # KafkaTemplate<String, Serializable>
│ │ │ │ ├── MongoConfiguration.java # MongoTemplate, MongoDatabaseFactory
│ │ │ │ ├── MongoRepositoryConfig.java # @EnableMongoRepositories
│ │ │ │ ├── MultiTenantJpaConfig.java # Multi-tenancy: TenantAwareDataSource, HikariCP
│ │ │ │ └── ResourceLockConfiguration.java # Bloqueos distribuidos via Redis
│ │ │ │
│ │ │ ├── constants/ # [TRANSVERSAL] Constantes de dominio
│ │ │ │ └── AccountingDomainConstants.java # Longitudes de campo, estados, claves i18n
│ │ │ │
│ │ │ ├── monitoring/ # [TRANSVERSAL] Observabilidad
│ │ │ │ └── ApplicationMetrics.java # Counters, Gauges, Timers, HealthIndicator
│ │ │ │
│ │ │ ├── util/ # [TRANSVERSAL] Utilidades
│ │ │ │ └── AccountingMessages.java # Resolucion de mensajes i18n via MessageSource
│ │ │ │
│ │ │ ├── v1/ # [VERSIONADO] API v1
│ │ │ │ │
│ │ │ │ ├── cdc/ # DOMINIO: Centro de Costos
│ │ │ │ │ ├── component/
│ │ │ │ │ │ ├── CentroCostosComponent.java # Logica de negocio CRUD
│ │ │ │ │ │ └── TipoCentroCostosComponent.java # Logica de negocio tipos
│ │ │ │ │ ├── controller/
│ │ │ │ │ │ ├── CentroCostosController.java # REST /api/v1/centro-costos
│ │ │ │ │ │ └── TipoCentroCostosController.java # REST /api/v1/tipo-centro-costos
│ │ │ │ │ ├── entity/
│ │ │ │ │ │ ├── CentroCostosEntity.java # JPA: tabla centros_costos
│ │ │ │ │ │ └── TipoCentroCostosEntity.java # JPA: tabla tipos_centro_costos
│ │ │ │ │ ├── lightloader/
│ │ │ │ │ │ └── CentroCostosLightLoader.java # Batch loading (prevencion N+1)
│ │ │ │ │ ├── repository/
│ │ │ │ │ │ ├── CentroCostosRepository.java # SimappeRepository + queries custom
│ │ │ │ │ │ └── TipoCentroCostosRepository.java # SimappeRepository
│ │ │ │ │ └── service/
│ │ │ │ │ ├── CentroCostosService.java # Fachada de servicio
│ │ │ │ │ └── TipoCentroCostosService.java # Fachada de servicio
│ │ │ │ │
│ │ │ │ └── kafka/ # COMPONENTES KAFKA (compartidos)
│ │ │ │ ├── AuditLogListener.java # @KafkaListener: user-events → MongoDB
│ │ │ │ └── AuditLogRepository.java # MongoRepository<AuditLog, String>
│ │ │ │
│ │ │ ├── NebulaAccountingCoreApplication.java # Clase principal @SpringBootApplication
│ │ │ └── ServletInitializer.java # Soporte despliegue WAR
│ │ │
│ │ └── resources/
│ │ ├── application.yml # Config base (nombre, perfil, config import)
│ │ ├── application-localhost.yml # Perfil desarrollo local
│ │ ├── application-dev.yml # Perfil desarrollo remoto
│ │ ├── application-prod.yml # Perfil produccion
│ │ ├── banner.txt # Banner de arranque ASCII
│ │ ├── messages_es.properties # Mensajes i18n en espanol
│ │ └── db/i18n/ # Scripts SQL de internacionalizacion
│ │ ├── V001__accounting_i18n_initial.sql
│ │ └── V002__accounting_i18n_codigo_duplicado.sql
│ │
│ └── test/
│ └── java/com/centrica/nebula/accounting/core/
│ ├── v1/cdc/
│ │ ├── component/ # Tests de componentes
│ │ │ ├── CentroCostosComponentTest.java
│ │ │ └── TipoCentroCostosComponentTest.java
│ │ ├── builder/ # Builders para datos de test
│ │ │ ├── CentroCostosTestBuilder.java
│ │ │ └── TipoCentroCostosTestBuilder.java
│ │ └── constants/
│ │ └── CdcTestConstants.java
│ └── util/
│ └── TestConstants.java # Constantes globales de test
│
├── pom.xml # Configuracion Maven
├── Dockerfile # Imagen Docker multi-stage
├── Jenkinsfile # Pipeline CI/CD
├── README.md
├── TESTING.md
├── USAGE.md
├── CLAUDE.md
├── create-test-module.sh # Script generador de modulos de test
├── deploy-centrica-dev.sh # Script de deploy a DEV
└── .gitignore
El paquete base sigue la convencion:
com.centrica.nebula.{dominio}.core
Ejemplos:
| Microservicio | Paquete Base |
|---|---|
nebula-accounting-core |
com.centrica.nebula.accounting.core |
nebula-hr-core |
com.centrica.nebula.hr.core |
nebula-purchasing |
com.centrica.nebula.purchasing |
nebula-inventory-ops |
com.centrica.nebula.inventory.ops |
Referencia: Estandares de Desarrollo — Seccion de nombrado de paquetes.
Clases de configuracion Spring (@Configuration). Estas clases se cargan al inicio del servicio.
| Clase | Responsabilidad | Anotaciones Clave |
|---|---|---|
AppConfig |
TimeZone (America/Bogota), PasswordEncoder, RequestContextListener | @Configuration |
MultiTenantJpaConfig |
Routing multi-tenant, EntityManagerFactory, TransactionManager, HikariCP | @Configuration, @EnableTransactionManagement |
MongoConfiguration |
MongoTemplate, MongoDatabaseFactory | @Configuration |
MongoRepositoryConfig |
Habilita repositorios MongoDB con implementacion Simappe | @Configuration, @EnableMongoRepositories |
AsyncConfig |
Pools de hilos para @Async y @Scheduled (core=5, max=15) | @Configuration, @EnableAsync, @EnableScheduling |
ClientConfig |
WebClient con filtros de logging y Bearer token, SimappeAdminWebClient | @Configuration |
KafkaAdapterConfig |
KafkaTemplate para envio de mensajes de auditoria | @Configuration |
ResourceLockConfiguration |
Bloqueos distribuidos via Redis, validacion de conectividad | @Configuration, @ConditionalOnProperty |
DockerMongoHealthConfig |
Health checks de MongoDB para entornos Docker | @Configuration |
Constantes de dominio usadas en validaciones y reglas de negocio:
public final class AccountingDomainConstants {
// Longitudes de campo
public static final int CENTRO_COSTOS_CODIGO_MAX_LENGTH = 10;
public static final int CENTRO_COSTOS_NOMBRE_MAX_LENGTH = 300;
// Estados por defecto
public static final String CENTRO_COSTOS_ESTADO_DEFAULT = "ACTIVO";
// Claves de mensajes i18n
public static final String MSG_ERROR_CODIGO_INMUTABLE = "error.codigo.inmutable";
public static final String MSG_ERROR_ELIMINACION_REFERENCIAS = "error.eliminacion.referencias";
}
Metricas custom con Micrometer:
requests.total, errors.total, success.totaloperations.active, operations.processedoperation.durationHealthIndicator para /actuator/healthUtilidades del servicio. Ejemplo: AccountingMessages resuelve mensajes internacionalizados:
@Component
public class AccountingMessages {
private final MessageSource messageSource;
public String get(String key) {
return messageSource.getMessage(key, null, LocaleContextHolder.getLocale());
}
// Atajos de negocio
public String errorCodigoInmutable() {
return get(AccountingDomainConstants.MSG_ERROR_CODIGO_INMUTABLE);
}
}
Dentro de v1/, el codigo se agrupa por dominio de funcionalidad (bounded context). Cada dominio tiene su propia jerarquia de capas.
v1/{abreviatura-dominio}/
| Dominio | Abreviatura | Descripcion |
|---|---|---|
| Centro de Costos | cdc |
Gestion de centros de costos y sus tipos |
| Terceros | terceros |
Clientes, proveedores, empleados |
| Plan de Cuentas | pdc |
Cuentas contables y jerarquias |
| Asientos | asientos |
Asientos contables y movimientos |
| Capa | Paquete | Responsabilidad | Herencia |
|---|---|---|---|
| Controller | controller/ |
Endpoints REST, validacion de entrada, anotaciones OpenAPI | Extiende SimappeController |
| Service | service/ |
Fachada, orquestacion, delegacion al component | @Service |
| Component | component/ |
Logica de negocio, transacciones, reglas, auditoria | Extiende SimappeService, implementa CrudHtppServletRequestComponent |
| Entity | entity/ |
Entidades JPA mapeadas a tablas del tenant | Extiende SequenceBusinessBaseEntity (v2.9.0+) |
| Repository | repository/ |
Acceso a datos, queries JPQL/nativas | Extiende SimappeRepository<Entity, Long> |
| LightLoader | lightloader/ |
Batch loading, prevencion N+1, enriquecimiento de DTOs | Extiende AbstractLoadModels<ID, DTO, ENTITY> |
Controller — CentroCostosController.java:
@RestController
@RequestMapping("/api/v1/centro-costos")
@Tag(name = "Centro de Costos", description = "Gestion de centros de costos")
public class CentroCostosController extends SimappeController {
private final CentroCostosService service;
@GetMapping("/read")
@Operation(summary = "Listar todos los centros de costos")
public ResponseEntity<List<CentroCostosDto>> read(HttpServletRequest request) {
return ResponseEntity.ok(service.read(request));
}
@PostMapping("/create")
@Operation(summary = "Crear un centro de costos")
public ResponseEntity<CentroCostosDto> create(
@RequestBody CentroCostosDto dto, HttpServletRequest request) {
return ResponseEntity.ok(service.create(dto, request));
}
// GET /get, PUT /update, DELETE /delete, POST /page, POST /page-response
}
Component — CentroCostosComponent.java:
@Component
@ConnectionContext // Activa multi-tenancy
@EntityIdSupport // Soporte de IDs
@RequiredArgsConstructor
public class CentroCostosComponent
extends SimappeService<CentroCostosDto, Long, CentroCostosEntity, CentroCostosRepository>
implements CrudHtppServletRequestComponent<CentroCostosDto, Long>,
PageResponseSupport<CentroCostosResponse> {
@Observed(name = "centro-costos.create")
@Timed(value = "centro-costos.create.time")
@Transactional
@Override
public CentroCostosDto create(CentroCostosDto dto, HttpServletRequest request) {
// Regla de negocio: codigo no puede estar duplicado
if (repository.existsByCodigo(dto.getCodigo())) {
throw new SimappeException(messages.errorCodigoDuplicado());
}
CentroCostosEntity entity = mapper.map(dto, CentroCostosEntity.class);
CentroCostosEntity saved = repository.save(entity);
auditLogService.sendMessage("CENTRO_COSTOS_CREATED", saved);
return lightLoader.loadForEntity(saved);
}
}
Entity — CentroCostosEntity.java:
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Table(name = "centros_costos", schema = "accounting")
@SequenceGenerator(name = "simappe_seq_gen",
sequenceName = "accounting.centros_costos_seq", allocationSize = 25)
public class CentroCostosEntity extends SequenceBusinessBaseEntity {
@Column(name = "codigo", nullable = false, length = 10)
private String codigo;
@Column(name = "nombre", nullable = false, length = 100)
private String nombre;
@Column(name = "id_tipo_centro_costos")
private Long idTipoCentroCostos;
@Enumerated(EnumType.STRING)
@Column(name = "estado_centro", nullable = false, length = 10)
private EstadoCentroCostosEnum estadoCentro;
}
IMPORTANTE: La entidad extiende
SequenceBusinessBaseEntity(SimappeCommons 2.9.0+) en lugar deBusinessBaseEntity<LongId>. Esto es obligatorio para compatibilidad con Oracle. Ver Guia de Entidades y Secuencias Oracle para detalles completos.
Repository — CentroCostosRepository.java:
public interface CentroCostosRepository
extends SimappeRepository<CentroCostosEntity, Long> {
Optional<CentroCostosEntity> findByCodigo(String codigo);
boolean existsByCodigo(String codigo);
List<CentroCostosEntity> findByEstadoCentro(String estado);
long countByIdTipoCentroCostos(Long tipoId);
@Query("SELECT c FROM CentroCostosEntity c ORDER BY c.codigo ASC")
List<CentroCostosEntity> findAllOrderByCodigo();
@Query("SELECT c FROM CentroCostosEntity c WHERE c.estadoCentro = 'ACTIVO'")
List<CentroCostosEntity> findAllActive();
}
Nota: El tipo de ID del repositorio es
Long(noLongId), coherente conSequenceBusinessBaseEntity.
LightLoader — CentroCostosLightLoader.java:
@Component
@RequiredArgsConstructor
public class CentroCostosLightLoader
extends AbstractLoadModels<Long, CentroCostosDto, CentroCostosEntity> {
// Estrategia: Extraer IDs unicos → Query batch → Enriquecimiento en memoria
// Resultado: 100 centros = 2 queries en lugar de 101 (prevencion N+1)
public CentroCostosDto loadForEntity(CentroCostosEntity entity) { ... }
public List<CentroCostosDto> loadForListEntity(List<CentroCostosEntity> entities) { ... }
}
Nota: El tipo de ID del LightLoader es
Long(noLongId), coherente conSequenceBusinessBaseEntity.
El paquete kafka/ dentro de v1/ contiene los componentes de mensajeria compartidos por todos los dominios del servicio:
// AuditLogListener.java — Consume eventos y persiste en MongoDB
@Component
public class AuditLogListener {
@KafkaListener(topics = "user-events")
public void listen(AuditLogMessage message) {
AuditLog document = transform(message);
auditLogRepository.save(document);
}
}
// AuditLogRepository.java — Repositorio MongoDB
public interface AuditLogRepository extends MongoRepository<AuditLog, String> { }
NebulaAccountingCoreApplication.java configura el arranque:
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class, // Multi-tenancy manual
HibernateJpaAutoConfiguration.class, // JPA manual
MongoAutoConfiguration.class // MongoDB manual
})
@EnableDiscoveryClient // Registro en Eureka
@EnableFeignClients(clients = { SimappeAdminClient.class })
@Import({ MultiTenantJpaConfig.class, MongoConfiguration.class })
@ComponentScan(basePackages = {
"com.centrica.nebula.accounting.core", // Paquetes del servicio
"co.simappe.commons" // Paquetes de SimappeCommons
})
@EntityScan(basePackages = {
"com.centrica.nebula.accounting.core.v1.cdc.entity",
"co.simappe.commons" // Entidades base de Simappe
})
@EnableJpaRepositories(
basePackages = "com.centrica.nebula.accounting.core.v1.cdc.repository",
repositoryBaseClass = SimappeRepositoryImpl.class
)
@EnableMongoRepositories(
basePackages = "com.centrica.nebula.accounting.core.v1.kafka"
)
public class NebulaAccountingCoreApplication {
public static void main(String[] args) {
SpringApplication.run(NebulaAccountingCoreApplication.class, args);
}
}
Al agregar un nuevo dominio, actualizar
@EntityScan,@EnableJpaRepositoriesy@ComponentScanpara incluir los nuevos paquetes.
spring:
application:
name: nebula-accounting-core
profiles:
active: common,localhost # Perfil local por defecto
main:
allow-bean-definition-overriding: true
config:
import: "optional:configserver:" # Import desde Config Server
| Perfil | Uso | Configuracion desde |
|---|---|---|
localhost |
Desarrollo en PC local | application-localhost.yml |
dev |
Desarrollo en servidor compartido | Config Server (http://simappe-config-server:8890) |
prod |
Produccion | Config Server (protegido) |
Los tests replican la estructura del codigo fuente con adiciones:
test/java/com/centrica/nebula/accounting/core/
├── v1/cdc/
│ ├── component/ # Tests unitarios de componentes
│ │ ├── CentroCostosComponentTest.java
│ │ └── TipoCentroCostosComponentTest.java
│ ├── builder/ # Test Builders (patron Builder para datos de test)
│ │ ├── CentroCostosTestBuilder.java
│ │ └── TipoCentroCostosTestBuilder.java
│ └── constants/ # Constantes de test por dominio
│ └── CdcTestConstants.java
└── util/
└── TestConstants.java # Constantes globales de test
Frameworks de testing:
@ExtendWith(MockitoExtension.class))Referencia: Guia Crear Feature Backend — Seccion de tests.
v1/nebula-models, nunca en el microserviciokafka/ compartido por todos los dominios del servicioReferencia completa: Reglas de Desarrollo de Servicios
| Version | Fecha | Autor | Descripcion |
|---|---|---|---|
| 1.0.0 | 2026-03-16 | Carlos Torres | Creacion del documento |
| 1.1.0 | 2026-04-06 | Carlos Torres | Actualizar entidades a SequenceBusinessBaseEntity, repositorios a Long, ejemplos alineados con Oracle |