Versión: 1.0
Fecha: 8 de Marzo, 2026
Estado: NORMATIVO
Arquitecto: Carlos Alberto Torres Camargo
Referencia Base: ESTANDARES_DESARROLLO.md, ESTANDARES_CALIDAD_OPERACION.md
- Simappe es la base, Nebula es el negocio. No se reinventa lo que ya existe en SimappeCommons.
- Patrón de 4 capas obligatorio. Controller → Service → Component → Repository. La lógica va en Component.
- Multi-tenancy obligatorio. Todo endpoint de negocio lleva
@ConnectionContext. Solo endpoints de sistema usan @IgnoreTenant.
- Contract-First. Los DTOs se definen ANTES de implementar. Viven en
nebula-models.
- Sin hardcodeo. Todo configurable via variables de entorno o Config Server.
- Español en dominio, inglés en código. Nombres de clases y métodos en inglés. Nombres de campos de negocio en español (
centroCostos, tipoCentroCostos).
| Componente |
Versión |
Uso |
| Java |
25 |
Lenguaje. Usar Records, Virtual Threads, Pattern Matching |
| Spring Boot |
3.5.11+ |
Framework base |
| Spring Cloud |
2025.0.0 |
Microservicios (Eureka, Config, Gateway) |
| Maven |
3.9+ |
Build tool |
| Lombok |
Latest |
@Data, @Builder, @RequiredArgsConstructor |
| MapStruct |
1.6.3 |
Mapeo DTO ↔ Entity (compile-time) |
| ModelMapper |
3.2.5 |
Mapeo DTO ↔ Entity (runtime, SimappeModelMapper) |
| SpringDoc OpenAPI |
2.8.5 |
Documentación de API |
| PostgreSQL |
16+ |
BD del sistema (SimappeAdmin) |
| Oracle (Centrica) |
Multi-motor |
BD de tenant (negocio) |
| MongoDB |
7+ |
Auditoría y logs |
| Redis |
7.2+ |
Caché distribuido |
| Kafka |
Latest |
Eventos y auditoría |
| Componente |
Versión |
Uso |
| Angular |
21 |
Framework SPA |
| TypeScript |
5.8+ |
Lenguaje tipado |
| Nx |
21.x |
Monorepo, boundary enforcement |
| NgRx Signal Store |
19+ |
State management de dominio |
| Angular Signals |
Built-in |
State management de componente |
| RxJS |
7.x |
Solo para asincronía (HTTP, WebSocket) |
| PrimeNG |
19+ |
Component library base |
| Tailwind CSS |
4.x |
Utility-first CSS |
| esbuild |
Built-in |
Bundler |
com.centrica.nebula.{dominio}.{subdominio}.v1/
├── config/ # @Configuration beans
│ ├── AppConfig.java
│ ├── MultiTenantJpaConfig.java
│ ├── MongoConfiguration.java
│ ├── RedisConfig.java
│ └── OpenApiConfig.java
├── {feature}/
│ ├── controller/
│ │ └── {Feature}Controller.java # @RestController
│ ├── service/
│ │ └── {Feature}Service.java # @Service (facade)
│ ├── component/
│ │ └── {Feature}Component.java # @Component (lógica)
│ ├── entity/
│ │ └── {Feature}Entity.java # @Entity JPA
│ ├── repository/
│ │ └── {Feature}Repository.java # extends SimappeRepository
│ └── lightloader/
│ └── {Feature}LightLoader.java # Enriquecimiento DTO (opcional)
├── kafka/
│ └── AuditLogProducer.java
├── document/
│ └── {Feature}AuditDocument.java # MongoDB document
└── monitoring/
└── ApplicationMetrics.java
| Elemento |
Convención |
Ejemplo |
| Microservicio |
nebula-{dominio} o nebula-{dominio}-{subdominio} |
nebula-accounting-core |
| Package |
com.centrica.nebula.{dominio}.{subdominio}.v1 |
com.centrica.nebula.accounting.core.v1 |
| Controller |
{Feature}Controller |
CentroCostosController |
| Service |
{Feature}Service |
CentroCostosService |
| Component |
{Feature}Component |
CentroCostosComponent |
| Repository |
{Feature}Repository |
CentroCostosRepository |
| Entity |
{Feature}Entity |
CentroCostosEntity |
| DTO |
{Feature}Dto (en nebula-models) |
CentroCostosDto |
| Tabla DB |
snake_case |
centro_costos |
| Schema DB |
nombre del dominio |
accounting, inventory |
| Endpoint |
/api/v1/{dominio}/{feature}/{accion} |
/api/v1/accounting/centro-costos/read |
// ✅ CORRECTO: Inyección por constructor con Lombok
@RequiredArgsConstructor
public class CentroCostosService {
private final CentroCostosComponent component;
}
// ❌ PROHIBIDO: Inyección por campo
public class CentroCostosService {
@Autowired
private CentroCostosComponent component; // NUNCA
}
// ✅ CORRECTO: Component extiende SimappeService
@Component
@ConnectionContext
@EntityIdSupport
@RequiredArgsConstructor
public class CentroCostosComponent extends SimappeService<
CentroCostosDto, Long, CentroCostosEntity, CentroCostosRepository
> implements CrudHtppServletRequestComponent<CentroCostosDto, Long> {
// Lógica de negocio aquí
}
// ❌ PROHIBIDO: Lógica de negocio en Controller o Service
@RestController
public class CentroCostosController {
public void create(CentroCostosDto dto) {
repository.save(entity); // NUNCA en controller
}
}
// ✅ CORRECTO: Endpoint multi-tenant
@GetMapping("/read")
@ConnectionContext
public ResponseEntity<List<CentroCostosDto>> read(HttpServletRequest request) {
return ResponseEntity.ok(service.read(request));
}
// ✅ CORRECTO: Endpoint de sistema (sin tenant)
@GetMapping("/health")
@IgnoreTenant
public ResponseEntity<String> health() {
return ResponseEntity.ok("OK");
}
| Prohibición |
Razón |
@Autowired en campos |
Usar @RequiredArgsConstructor |
System.out.println |
Usar @Slf4j de Lombok |
any en frontend |
Usar interfaces tipadas |
| Lógica en Controller |
Va en Component |
SQL nativo sin @Query |
Usar JPA/JPQL o Spring Data methods |
new RestTemplate() |
Usar WebClient |
Thread.sleep() |
Usar programación reactiva si necesita esperar |
| Hardcodear URLs, puertos, credenciales |
Variables de entorno |
| Modificar SimappeCommons sin aprobación |
Requiere PR al Arquitecto |
| Crear servicio sin arquetipo |
Usar SimappeArchetype |
// ✅ CORRECTO: Usar SimappeException
@Transactional
public CentroCostosDto create(CentroCostosDto dto, HttpServletRequest request) {
if (repository.existsByCodigo(dto.getCodigo())) {
throw new SimappeException("Centro de costos con código " + dto.getCodigo() + " ya existe");
}
// ...
}
// ✅ CORRECTO: Transaccionalidad explícita
@Transactional(rollbackFor = Exception.class)
public void createWithDetails(AsientoDto dto, HttpServletRequest request) {
AsientoEntity asiento = repository.save(mapToEntity(dto));
detalleRepository.saveAll(mapDetalles(dto.getDetalles(), asiento));
// Si algo falla, TODO hace rollback
}
spring:
application:
name: nebula-{servicio}
profiles:
active: ${APP_PROFILE:dev}
# PostgreSQL (System DB - SimappeAdmin)
datasource:
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:simappe_admin}
username: ${POSTGRES_USER:postgres}
password: ${POSTGRES_PASSWORD:postgres}
# MongoDB (Audit)
data:
mongodb:
uri: mongodb://${MONGO_USER:sysadmin}:${MONGO_PASSWORD:centrica2026}@${MONGO_HOST:localhost}:${MONGO_PORT:27017}/${MONGO_DB:simappe_audit}?authSource=admin
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
jpa:
hibernate:
ddl-auto: none
show-sql: false
properties:
hibernate:
format_sql: true
default_schema: ${DB_SCHEMA:public}
# Eureka
eureka:
client:
serviceUrl:
defaultZone: http://${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}/eureka/
registerWithEureka: true
fetchRegistry: true
instance:
preferIpAddress: true
instance-id: ${spring.application.name}:${random.value}
# Kafka
kafka:
bootstrapAddress: ${KAFKA_HOST:localhost}:${KAFKA_PORT:9092}
# Server
server:
port: ${SERVER_PORT:8080}
# OpenAPI
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
# Stage 1: Build
FROM maven:3.9-eclipse-temurin-25 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
# Stage 2: Runtime
FROM eclipse-temurin:25-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
# Health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
pipeline {
agent any
environment {
DOCKER_REGISTRY = '10.110.0.2:5000'
SERVICE_NAME = 'nebula-{servicio}'
}
stages {
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
jacoco execPattern: '**/target/jacoco.exec'
}
}
}
stage('SonarQube') {
steps {
withSonarQubeEnv('SonarQube') {
sh 'mvn sonar:sonar'
}
}
}
stage('Docker Build') {
steps {
sh "docker build -t ${DOCKER_REGISTRY}/${SERVICE_NAME}:${BUILD_NUMBER} ."
sh "docker tag ${DOCKER_REGISTRY}/${SERVICE_NAME}:${BUILD_NUMBER} ${DOCKER_REGISTRY}/${SERVICE_NAME}:latest"
}
}
stage('Docker Push') {
steps {
sh "docker push ${DOCKER_REGISTRY}/${SERVICE_NAME}:${BUILD_NUMBER}"
sh "docker push ${DOCKER_REGISTRY}/${SERVICE_NAME}:latest"
}
}
stage('Deploy DEV') {
when { branch 'develop' }
steps {
sh "ssh sysadmin@10.120.0.2 'docker pull ${DOCKER_REGISTRY}/${SERVICE_NAME}:latest && docker-compose -f /opt/nebula/docker-compose.yml up -d ${SERVICE_NAME}'"
}
}
}
}
Ver documento completo: ARQUITECTURA_FRONTEND_NEBULA.md
// ✅ OBLIGATORIO: Standalone + OnPush
@Component({
standalone: true,
imports: [DataTableComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `...`
})
// ❌ PROHIBIDO: NgModule
@NgModule({ declarations: [MyComponent] }) // NUNCA
// ✅ OBLIGATORIO: Signals para estado
private _items = signal<Item[]>([]);
readonly items = this._items.asReadonly();
// ❌ PROHIBIDO: BehaviorSubject para estado local
private items$ = new BehaviorSubject<Item[]>([]); // Solo si es async complejo
// ✅ OBLIGATORIO: inject() function
private service = inject(ItemService);
// ❌ PROHIBIDO: Constructor injection en componentes
constructor(private service: ItemService) {} // Preferir inject()
// ✅ OBLIGATORIO: Control flow nativo
@if (loading()) { <spinner /> }
@for (item of items(); track item.id) { <row /> }
// ❌ PROHIBIDO: Directivas estructurales legacy
*ngIf="loading" // NUNCA en código nuevo
*ngFor="let item of items" // NUNCA en código nuevo
// ✅ OBLIGATORIO: HTTP solo en services
@Injectable({ providedIn: 'root' })
export class ItemService {
private http = inject(HttpClient);
// HTTP calls AQUÍ
}
// ❌ PROHIBIDO: HTTP en componentes
export class ItemComponent {
private http = inject(HttpClient); // NUNCA en componente
}
feature-{entidad}/
├── {entidad}-list/
│ ├── {entidad}-list.component.ts # Smart: DataTable + filtros
│ └── {entidad}-list.component.html
├── {entidad}-new/
│ ├── {entidad}-new.component.ts # Smart: Formulario creación
│ └── {entidad}-new.component.html
├── {entidad}-edit/
│ ├── {entidad}-edit.component.ts # Smart: Formulario edición
│ └── {entidad}-edit.component.html
├── {entidad}-view/
│ ├── {entidad}-view.component.ts # Smart: Vista detalle (readonly)
│ └── {entidad}-view.component.html
└── {entidad}.routes.ts # Routing lazy-loaded
/ E2E \ 5% - Playwright (flujos críticos)
/─────────\
/ Integration\ 15% - Spring Boot Test + TestContainers
/──────────────\
/ Unit Tests \ 80% - JUnit 5 + Mockito (backend) / Jest (frontend)
/──────────────────\
| Capa |
Backend |
Frontend |
| Component (lógica) |
80% |
N/A |
| Service |
70% |
70% |
| Controller |
60% |
N/A |
| Feature components |
N/A |
60% |
| Shared/UI |
N/A |
70% |
| Util (pipes, helpers) |
90% |
90% |
| Métrica |
Umbral |
| Bugs |
0 |
| Vulnerabilities |
0 |
| Code Smells |
< 20 por servicio |
| Coverage |
≥ 70% |
| Duplications |
< 5% |
| Maintainability Rating |
A |
main ─────────────────────────────────────────────────
│ ↑
└── release/1.0.0 ────────────────────────┘
↑
develop ──────────────────────────────────────────────
│ ↑ ↑ ↑
├── feature/NEB-01 ───┘ │
├── feature/NEB-02 ──────────────┘
└── bugfix/NEB-03 ──────────────────────────────────
| Branch |
Desde |
Merge a |
Convención |
feature/NEB-{id}-descripcion |
develop |
develop |
Nueva funcionalidad |
bugfix/NEB-{id}-descripcion |
develop |
develop |
Corrección |
release/X.Y.Z |
develop |
main + develop |
Preparación release |
hotfix/NEB-{id}-descripcion |
main |
main + develop |
Fix urgente en producción |
feat(accounting): add CRUD for centro de costos
fix(treasury): correct bank reconciliation calculation
refactor(masters): simplify terceros search query
test(accounting): add unit tests for asiento validation
docs(inventory): update API documentation for kardex
chore(ci): update Jenkins pipeline for SonarQube integration
Formato: {tipo}({scope}): {descripción en minúsculas}
| Tipo |
Uso |
feat |
Nueva funcionalidad |
fix |
Corrección de bug |
refactor |
Refactorización sin cambio de comportamiento |
test |
Agregar o corregir tests |
docs |
Documentación |
chore |
Configuración, CI, dependencias |
perf |
Mejora de rendimiento |
- Título: Mismo formato que conventional commits
- Descripción: Qué cambia, por qué, cómo probar
- Labels:
domain:{dominio}, stack:{backend|frontend}, type:{feature|fix|refactor}
- Asignado a: Gatekeeper correspondiente (Sr. Backend o Sr. Frontend)
- Tamaño: Máximo ~400 líneas de código cambiado. Si es más grande → dividir en PRs
- Tests: Incluir tests. Pipeline debe estar verde
- Sin conflictos: Resolver conflictos antes de solicitar review
// ✅ CORRECTO: Logging estructurado con Slf4j
@Slf4j
public class CentroCostosComponent {
public CentroCostosDto create(CentroCostosDto dto, HttpServletRequest request) {
log.info("Creando centro de costos: codigo={}, nombre={}", dto.getCodigo(), dto.getNombre());
// ...
log.debug("Centro de costos creado: id={}", saved.getId());
return result;
}
}
// ❌ PROHIBIDO
System.out.println("Centro de costos creado"); // NUNCA
log.info("Centro de costos: " + dto.toString()); // NUNCA (concatenación)
log.info("Password: {}", dto.getPassword()); // NUNCA (datos sensibles)
Todo servicio debe exponer /actuator/health con indicadores de:
- Base de datos (JPA)
- Redis
- Kafka
- MongoDB
- Disco
- Eureka registration
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
- Todo endpoint requiere JWT válido (excepto
/actuator/health y /oauth/token)
- El JWT contiene:
userId, roles, clientId, companyId, environment
- El Gateway valida el JWT antes de rutear al servicio
- Los servicios confían en el Gateway (no re-validan JWT)
| Dato |
Regla |
| Contraseñas |
Encriptadas con BCrypt. NUNCA en logs |
| Tokens |
NUNCA en logs. NUNCA hardcodeados |
| Credenciales de BD |
Variables de entorno. NUNCA en código |
| Datos personales (PII) |
Ofuscar en logs. Cumplir regulación |
| Claves API externas |
Config Server o Vault. NUNCA en código |
Antes de crear PR, verificar:
| Version |
Fecha |
Autor |
Descripcion |
| 1.0.0 |
2026-03-08 |
Carlos Torres |
Creación del documento consolidado de reglas de desarrollo |