21 KiB
21 KiB
LDPv2 - Technical Setup Guide
Overview
This guide provides the technical foundation for the LDPv2 project, including technology stack details, project structure, and initial setup instructions.
Technology Stack
Backend
- Framework: Spring Boot 3.2.x
- Language: Java 17 or 21
- Security: Spring Security 6.x with JWT
- Persistence:
- PostgreSQL 16
- Hibernate 6.x (JPA)
- HikariCP (connection pooling)
- Migration: Liquibase 4.x
- Build Tool: Maven 3.9.x
- Testing:
- JUnit 5
- Mockito
- Testcontainers (for integration tests)
- REST Assured (for API tests)
- Documentation: SpringDoc OpenAPI (Swagger)
- Validation: Jakarta Bean Validation
Frontend
- Framework: Angular 18
- Language: TypeScript 5.x
- UI Library: Angular Material or PrimeNG (to be decided)
- HTTP Client: Angular HttpClient
- State Management: Service-based (or NgRx for complex state)
- Forms: Reactive Forms
- Routing: Angular Router
- Testing:
- Jasmine/Karma (unit tests)
- Cypress or Playwright (E2E tests)
- Build Tool: Angular CLI
DevOps
- Containerization: Docker
- Container Orchestration: Docker Compose (dev), Kubernetes (prod)
- CI/CD: Jenkins, GitLab CI, or GitHub Actions
- Version Control: Git
Project Structure
Backend Structure
ldpv2-backend/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/ldpv2/
│ │ │ ├── LdpV2Application.java
│ │ │ ├── config/
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ ├── JwtConfig.java
│ │ │ │ └── OpenApiConfig.java
│ │ │ ├── domain/
│ │ │ │ ├── entity/
│ │ │ │ │ ├── BaseEntity.java
│ │ │ │ │ ├── Application.java
│ │ │ │ │ ├── Environment.java
│ │ │ │ │ └── ...
│ │ │ │ └── enums/
│ │ │ │ ├── ApplicationStatus.java
│ │ │ │ └── ExternalDependencyType.java
│ │ │ ├── repository/
│ │ │ │ ├── ApplicationRepository.java
│ │ │ │ ├── EnvironmentRepository.java
│ │ │ │ └── ...
│ │ │ ├── service/
│ │ │ │ ├── ApplicationService.java
│ │ │ │ ├── EnvironmentService.java
│ │ │ │ └── ...
│ │ │ ├── dto/
│ │ │ │ ├── request/
│ │ │ │ │ ├── CreateApplicationRequest.java
│ │ │ │ │ └── ...
│ │ │ │ └── response/
│ │ │ │ ├── ApplicationResponse.java
│ │ │ │ └── ...
│ │ │ ├── controller/
│ │ │ │ ├── ApplicationController.java
│ │ │ │ ├── EnvironmentController.java
│ │ │ │ └── ...
│ │ │ ├── security/
│ │ │ │ ├── JwtTokenProvider.java
│ │ │ │ ├── JwtAuthenticationFilter.java
│ │ │ │ └── UserDetailsServiceImpl.java
│ │ │ └── exception/
│ │ │ ├── GlobalExceptionHandler.java
│ │ │ ├── ResourceNotFoundException.java
│ │ │ └── ...
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ └── db/
│ │ └── changelog/
│ │ ├── db.changelog-master.xml
│ │ ├── v1.0/
│ │ │ ├── 001-create-base-tables.xml
│ │ │ ├── 002-create-application-tables.xml
│ │ │ └── ...
│ │ └── data/
│ │ └── initial-data.xml
│ └── test/
│ └── java/
│ └── com/ldpv2/
│ ├── integration/
│ │ ├── ApplicationIntegrationTest.java
│ │ └── ...
│ ├── service/
│ │ ├── ApplicationServiceTest.java
│ │ └── ...
│ └── controller/
│ ├── ApplicationControllerTest.java
│ └── ...
├── pom.xml
├── Dockerfile
└── docker-compose.yml
Frontend Structure
ldpv2-frontend/
├── src/
│ ├── app/
│ │ ├── core/
│ │ │ ├── auth/
│ │ │ │ ├── auth.service.ts
│ │ │ │ ├── auth.guard.ts
│ │ │ │ ├── jwt.interceptor.ts
│ │ │ │ └── login/
│ │ │ │ ├── login.component.ts
│ │ │ │ ├── login.component.html
│ │ │ │ └── login.component.scss
│ │ │ ├── services/
│ │ │ │ ├── api.service.ts
│ │ │ │ └── error-handler.service.ts
│ │ │ └── interceptors/
│ │ │ ├── jwt.interceptor.ts
│ │ │ └── error.interceptor.ts
│ │ ├── shared/
│ │ │ ├── models/
│ │ │ │ ├── application.model.ts
│ │ │ │ ├── environment.model.ts
│ │ │ │ └── ...
│ │ │ ├── components/
│ │ │ │ ├── header/
│ │ │ │ ├── footer/
│ │ │ │ └── loading-spinner/
│ │ │ └── pipes/
│ │ │ └── date-format.pipe.ts
│ │ ├── features/
│ │ │ ├── applications/
│ │ │ │ ├── application.service.ts
│ │ │ │ ├── application-list/
│ │ │ │ │ ├── application-list.component.ts
│ │ │ │ │ ├── application-list.component.html
│ │ │ │ │ └── application-list.component.scss
│ │ │ │ ├── application-detail/
│ │ │ │ └── application-form/
│ │ │ ├── environments/
│ │ │ │ ├── environment.service.ts
│ │ │ │ └── ...
│ │ │ ├── deployments/
│ │ │ └── business-units/
│ │ ├── app.component.ts
│ │ ├── app.component.html
│ │ ├── app.routes.ts
│ │ └── app.config.ts
│ ├── assets/
│ │ ├── images/
│ │ └── i18n/
│ ├── environments/
│ │ ├── environment.ts
│ │ └── environment.prod.ts
│ ├── styles.scss
│ └── index.html
├── angular.json
├── package.json
├── tsconfig.json
└── Dockerfile
Initial Setup
Prerequisites
- JDK 17 or 21
- Node.js 18+ and npm
- Docker and Docker Compose
- PostgreSQL 16 (or use Docker)
- Git
- IDE (IntelliJ IDEA, VS Code, or similar)
Backend Setup
1. Create Spring Boot Project
# Using Spring Initializr or
# Download from https://start.spring.io with:
# - Spring Boot 3.2.x
# - Dependencies: Web, Security, JPA, PostgreSQL, Liquibase, Validation
2. Configure application.yml
spring:
application:
name: ldpv2-backend
datasource:
url: jdbc:postgresql://localhost:5432/ldpv2
username: ldpv2_user
password: ldpv2_password
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
jpa:
hibernate:
ddl-auto: validate # Let Liquibase handle schema
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
liquibase:
change-log: classpath:db/changelog/db.changelog-master.xml
enabled: true
jwt:
secret: ${JWT_SECRET:your-secret-key-change-in-production}
expiration: 3600000 # 1 hour in milliseconds
server:
port: 8080
servlet:
context-path: /api
logging:
level:
com.ldpv2: DEBUG
org.springframework.security: DEBUG
3. Docker Compose for Development
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: ldpv2-postgres
environment:
POSTGRES_DB: ldpv2
POSTGRES_USER: ldpv2_user
POSTGRES_PASSWORD: ldpv2_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- ldpv2-network
backend:
build: ./ldpv2-backend
container_name: ldpv2-backend
depends_on:
- postgres
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/ldpv2
SPRING_DATASOURCE_USERNAME: ldpv2_user
SPRING_DATASOURCE_PASSWORD: ldpv2_password
JWT_SECRET: development-secret-key
ports:
- "8080:8080"
networks:
- ldpv2-network
volumes:
postgres_data:
networks:
ldpv2-network:
driver: bridge
Frontend Setup
1. Create Angular Project
npm install -g @angular/cli@18
ng new ldpv2-frontend
cd ldpv2-frontend
2. Install Dependencies
# Angular Material (or PrimeNG)
ng add @angular/material
# Additional dependencies
npm install --save \
@angular/common \
@angular/forms \
rxjs
3. Configure Environment
// src/environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:8080/api'
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: '/api'
};
4. Proxy Configuration for Development
// proxy.conf.json
{
"/api": {
"target": "http://localhost:8080",
"secure": false,
"changeOrigin": true
}
}
Update angular.json:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.json"
}
}
Database Schema Management
Liquibase Changelog Structure
<!-- db.changelog-master.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<include file="db/changelog/v1.0/001-create-base-tables.xml"/>
<include file="db/changelog/v1.0/002-create-application-tables.xml"/>
<include file="db/changelog/v1.0/003-create-deployment-tables.xml"/>
<!-- Add more as stories are developed -->
</databaseChangeLog>
Example Migration File
<!-- 001-create-base-tables.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="001-create-base-entities" author="ldpv2-team">
<!-- Enable UUID extension -->
<sql>CREATE EXTENSION IF NOT EXISTS "uuid-ossp";</sql>
<!-- Create Business Unit table -->
<createTable tableName="business_unit">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false" unique="true"/>
</column>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<!-- Create Environment table -->
<createTable tableName="environment">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="VARCHAR(100)">
<constraints nullable="false" unique="true"/>
</column>
<column name="description" type="TEXT"/>
<column name="is_production" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="criticality_level" type="INTEGER"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<!-- Indexes -->
<createIndex tableName="environment" indexName="idx_env_is_production">
<column name="is_production"/>
</createIndex>
</changeSet>
</databaseChangeLog>
Security Configuration
JWT Token Provider Example
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration;
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (SignatureException | MalformedJwtException | ExpiredJwtException |
UnsupportedJwtException | IllegalArgumentException ex) {
return false;
}
}
}
Security Configuration Example
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
API Documentation (OpenAPI/Swagger)
Configuration
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI ldpV2OpenAPI() {
return new OpenAPI()
.info(new Info()
.title("LDPv2 API")
.description("Lifecycle Data Platform v2 - Application Management API")
.version("1.0.0")
.contact(new Contact()
.name("LDPv2 Team")
.email("team@ldpv2.com"))
.license(new License()
.name("Proprietary")
.url("https://ldpv2.com/license")))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
Access Swagger UI at: http://localhost:8080/swagger-ui/index.html
Development Workflow
Backend Development Cycle
- Create Liquibase migration
- Run migration:
mvn liquibase:update - Generate JPA entities
- Create repository interfaces
- Implement service layer
- Create DTOs
- Implement controller
- Write tests
- Document API with Swagger annotations
- Commit and push
Frontend Development Cycle
- Create TypeScript models
- Implement Angular service
- Create components (list, detail, form)
- Add routing
- Style with CSS/SCSS
- Write tests
- Commit and push
Testing Strategy
Backend Testing
// Unit Test Example
@ExtendWith(MockitoExtension.class)
class ApplicationServiceTest {
@Mock
private ApplicationRepository applicationRepository;
@InjectMocks
private ApplicationService applicationService;
@Test
void shouldCreateApplication() {
// Test implementation
}
}
// Integration Test Example with Testcontainers
@SpringBootTest
@Testcontainers
class ApplicationIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void shouldSaveAndRetrieveApplication() {
// Test implementation
}
}
Frontend Testing
// Component Unit Test
describe('ApplicationListComponent', () => {
let component: ApplicationListComponent;
let fixture: ComponentFixture<ApplicationListComponent>;
let mockApplicationService: jasmine.SpyObj<ApplicationService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('ApplicationService', ['getApplications']);
await TestBed.configureTestingModule({
imports: [ApplicationListComponent],
providers: [
{ provide: ApplicationService, useValue: spy }
]
}).compileComponents();
mockApplicationService = TestBed.inject(ApplicationService) as jasmine.SpyObj<ApplicationService>;
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Running the Application
Development Mode
Backend
# Using Maven
mvn spring-boot:run
# Using Docker Compose
docker-compose up
Frontend
# Development server
ng serve
# With proxy
ng serve --proxy-config proxy.conf.json
# Access at http://localhost:4200
Production Build
Backend
mvn clean package
java -jar target/ldpv2-backend-1.0.0.jar
Frontend
ng build --configuration production
# Output in dist/ directory
Next Steps
After completing this technical setup:
- Proceed to Story 0 (Foundation) to build the Walking Skeleton
- Follow the story sequence for iterative development
- Maintain test coverage throughout
- Document APIs as you build
- Commit frequently with meaningful messages
Document Version: 1.0
Last Updated: February 2026
Status: Ready for Development