ADR-006: OpenAPI Contract-First Code Generation
Status: Accepted
Context
BATbern is a polyglot monorepo with:
- Backend: Multiple Java/Spring Boot microservices (company-user-management, event-management, partner-coordination, etc.)
- Frontend: React 19 + TypeScript SPA
- API Documentation: OpenAPI 3.1 specifications in
docs/api/*.openapi.yml
Problems Without Contract-First Approach
- Type Inconsistency: Backend DTOs and frontend TypeScript types could drift apart
- Manual Boilerplate: Creating DTOs and API interfaces manually is tedious and error-prone
- Documentation Lag: OpenAPI specs often became outdated after code changes
- No Contract Enforcement: No build-time validation that implementation matches specification
- Frontend-Backend Mismatch: Field names, types, or validations could differ between layers
- Testing Gaps: API contract tests couldnβt reliably catch interface changes
Key Requirements
- Single Source of Truth: OpenAPI specification must drive both frontend and backend code
- Type Safety: Guarantee that frontend types match backend DTOs
- Build-Time Validation: Compilation should fail if implementation diverges from contract
- Architecture Compliance: Must integrate with shared-kernel infrastructure (ErrorResponse, PaginationMetadata)
- Developer Experience: Reduce boilerplate while maintaining control over business logic
- Maintainability: API changes should automatically propagate to implementation
Decision
We have adopted a hybrid contract-first approach using OpenAPI code generation for both frontend and backend, with the following principles:
1. OpenAPI Specification as Single Source of Truth
- All API contracts defined in
docs/api/*.openapi.yml(OpenAPI 3.1 format) - Specifications use shared-kernel types via imports (ErrorResponse, PaginationMetadata)
- Frontend and backend both generate code from the same OpenAPI specs
- Specifications maintained alongside code, versioned in Git
1a. Shared schemas: ONE definition via $ref to _shared.openapi.yml (decided 2026-06-26)
Common schemas (ErrorResponse, PaginationMetadata, PaginatedResponse, ValidationError)
are defined once in docs/api/_shared.openapi.yml β with their full properties plus
the x-java-type vendor extension β and every spec references them by $ref:
components:
schemas:
ErrorResponse:
$ref: './_shared.openapi.yml#/components/schemas/ErrorResponse'
PaginationMetadata:
$ref: './_shared.openapi.yml#/components/schemas/PaginationMetadata'
This is the only sanctioned pattern, because it satisfies both generators (verified):
- Backend β
openapi-generator: resolves the external$ref, then theschemaMappings/importMappingsbelow map the schema by name to the shared-kernel class β no duplicate DTO is generated. (Mapping is name-based, independent of whether the schema has a body.) - Frontend β
openapi-typescript7.x: resolves the external$ref(built-in Redocly bundling) β emits the full TypeScript type.
Anti-pattern (do NOT use): a body-less βstubβ schema (type: object + x-java-type, no
properties). It works for the Java side (name mapping) but openapi-typescript turns it into
an empty Record<string, never>, silently breaking every frontend consumer. The canonical
schema must keep its properties so the TS type is real; the $ref keeps it DRY.
2. Backend: Generated Interfaces + Manual Implementation
Pattern: Generate API interfaces and DTOs, implement manually
Technology: OpenAPI Generator with Spring Boot 3 generator
What Gets Generated (per service):
build/generated/src/main/java/
βββ ch/batbern/{service}/
βββ api/generated/ # API Interfaces
β βββ UsersApi.java
β βββ CompaniesApi.java
β βββ EventsApi.java
βββ dto/generated/ # DTOs
βββ UserResponse.java
βββ CreateUserRequest.java
βββ UpdateUserRequest.java
βββ ...
Gradle Configuration (from company-user-management-service/build.gradle):
plugins {
id 'org.openapi.generator' version '7.2.0'
}
openApiGenerate {
generatorName = "spring"
inputSpec = "$rootDir/docs/api/users-api.openapi.yml".toString()
outputDir = "$buildDir/generated".toString()
apiPackage = "ch.batbern.companyuser.api.generated"
modelPackage = "ch.batbern.companyuser.dto.generated"
// Import shared-kernel types (NOT generated)
importMappings = [
'ErrorResponse': 'ch.batbern.shared.dto.ErrorResponse',
'PaginationMetadata': 'ch.batbern.shared.api.PaginationMetadata',
'PaginatedResponse': 'ch.batbern.shared.dto.PaginatedResponse'
]
typeMappings = [
'ErrorResponse': 'ch.batbern.shared.dto.ErrorResponse',
'PaginationMetadata': 'ch.batbern.shared.api.PaginationMetadata'
]
schemaMappings = [
'ErrorResponse': 'ch.batbern.shared.dto.ErrorResponse',
'PaginationMetadata': 'ch.batbern.shared.api.PaginationMetadata'
]
configOptions = [
interfaceOnly: "true", // Only generate interfaces
useTags: "true", // Organize APIs by OpenAPI tags
useSpringBoot3: "true", // Spring Boot 3 annotations
useBeanValidation: "true", // @Valid, @NotNull
useJakartaEe: "true", // Jakarta EE (Spring Boot 3)
dateLibrary: "java8", // java.time.* types
generateBuilders: "true", // Generate builder pattern for DTOs
skipDefaultInterface: "true" // No default methods
]
}
// Add generated sources to compile
sourceSets.main.java.srcDirs += "$buildDir/generated/src/main/java"
// Generate before compile
compileJava.dependsOn tasks.openApiGenerate
Implementation Pattern:
// Controller implements generated interface
@RestController
@RequiredArgsConstructor
public class UserController implements UsersApi { // Generated interface
private final UserService userService;
private final IncludeParser includeParser; // From shared-kernel
@Override // Method signature enforced by generated interface
public ResponseEntity<UserResponse> getCurrentUser(Optional<String> include) {
Set<String> includes = includeParser.parse(include.orElse(null));
UserResponse user = userService.getCurrentUser(includes);
return ResponseEntity.ok(user);
}
@Override
public ResponseEntity<UserResponse> updateCurrentUser(UpdateUserRequest request) {
// Use generated DTO
UserResponse updated = userService.updateCurrentUser(request);
return ResponseEntity.ok(updated);
}
}
Service Layer Returns Generated DTOs:
@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserResponseMapper responseMapper; // Pure mapper component
private final DomainEventPublisher eventPublisher; // From shared-kernel
// Service returns generated DTOs (not entities)
public UserResponse updateCurrentUser(UpdateUserRequest request) {
// UpdateUserRequest is generated from OpenAPI spec
User user = getCurrentUserEntity();
// Update from generated DTO
if (request.getFirstName() != null) {
user.setFirstName(request.getFirstName());
}
if (request.getLastName() != null) {
user.setLastName(request.getLastName());
}
User saved = userRepository.save(user);
// Publish event using shared-kernel
eventPublisher.publishEvent(new UserUpdatedEvent(saved.getId(), saved.getEmail()));
return responseMapper.mapToResponse(saved); // Returns generated UserResponse DTO
}
}
Pure Mapper Pattern (Extracted Component for EntityβDTO Conversion):
/**
* Pure mapper for converting between User entities and generated DTOs.
*
* Pattern: Pure Mapper
* - Field mapping only
* - Type conversions (LocalDateTime β OffsetDateTime)
* - NO business logic
* - NO repository dependencies
* - NO complex data enrichment
*
* Business logic belongs in Service layer or dedicated business logic services.
*/
@Component
public class UserResponseMapper {
public UserResponse mapToResponse(User user) {
// Map JPA entity to generated DTO
UserResponse response = new UserResponse();
response.setId(user.getId());
response.setEmail(user.getEmail());
response.setFirstName(user.getFirstName());
response.setLastName(user.getLastName());
response.setCreatedAt(toOffsetDateTime(user.getCreatedAt())); // Type conversion
response.setUpdatedAt(toOffsetDateTime(user.getUpdatedAt()));
// ... more field mappings
return response;
}
private OffsetDateTime toOffsetDateTime(LocalDateTime localDateTime) {
return localDateTime != null
? localDateTime.atZone(ZoneId.systemDefault()).toOffsetDateTime()
: null;
}
}
Layered Architecture & Data Flow:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 1: CONTROLLER (API Boundary) β
β - Accepts: Generated DTOs (from OpenAPI) β
β - Returns: Generated DTOs (for OpenAPI compliance) β
β - Uses: Service methods (which return generated DTOs) β
β - Uses: Mapper (only if direct entity conversion needed) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 2: MAPPER (Pure Conversion Logic) β
β - toDto(Entity) β Generated DTO β
β - toEntity(Generated DTO) β Entity β
β - Type conversions: LocalDateTime β OffsetDateTime β
β - NO business logic β
β - NO repository dependencies (pure mapper pattern) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 3: SERVICE (Business Logic & Data Enrichment) β
β - Works with: Entities internally β
β - Returns: Generated DTOs to controller β
β - Uses: Mapper to convert entities β DTOs before returning β
β - Uses: Repository for data access β
β - Contains: Business logic (calculations, enrichment) β
β - Handles: UUID β topicCode conversions (batch queries) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 4: REPOSITORY (Data Access) β
β - Works with: Entities β
β - Returns: Entities β
β - JPA/Hibernate database interaction β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 5: ENTITY (Domain Model) β
β - JPA annotations β
β - Internal UUID + External meaningful ID (ADR-003) β
β - Never exposed directly to API β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 6: DATABASE (PostgreSQL) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Business Logic Separation:
- Business Logic (colorZone, status calculation, enrichment) β Service layer (Layer 3)
- Mapper (Layer 2) β Pure mapping only (entity β DTO conversions, type transformations)
- Service (Layer 3) β Returns generated DTOs (not entities), uses mapper for conversion
Example with business logic in service:
@Service
public class TopicService {
private final TopicRepository topicRepository;
private final TopicMapper topicMapper; // Pure mapper
public ch.batbern.events.dto.generated.topics.Topic getTopicByCode(String topicCode) {
Topic entity = topicRepository.findByTopicCode(topicCode)
.orElseThrow(() -> new TopicNotFoundException(topicCode));
// Service handles business logic and data enrichment
var similarityScores = convertSimilarityScoresToDtos(entity.getSimilarityScores());
// Mapper does pure entityβDTO conversion
return topicMapper.toDtoWithSimilarityScores(entity, similarityScores);
}
// Business logic method in service
public List<SimilarityScore> convertSimilarityScoresToDtos(List<Topic.SimilarityScore> entityScores) {
// Batch fetch topics to avoid N+1 queries
List<UUID> topicIds = entityScores.stream()
.map(Topic.SimilarityScore::getTopicId)
.collect(Collectors.toList());
List<Topic> topics = topicRepository.findAllById(topicIds);
Map<UUID, String> uuidToCodeMap = topics.stream()
.collect(Collectors.toMap(Topic::getId, Topic::getTopicCode));
// Convert UUID to topicCode (data enrichment)
return entityScores.stream()
.map(score -> {
SimilarityScore dto = new SimilarityScore();
dto.setTopicCode(uuidToCodeMap.get(score.getTopicId()));
dto.setScore(score.getScore().floatValue());
return dto;
})
.filter(dto -> dto.getTopicCode() != null)
.collect(Collectors.toList());
}
// Pure business logic (can be static utility)
public static TopicColorZone calculateColorZone(Integer staleness) {
if (staleness == null) return TopicColorZone.GRAY;
if (staleness < 50) return TopicColorZone.RED;
else if (staleness <= 83) return TopicColorZone.YELLOW;
else return TopicColorZone.GREEN;
}
}
3. Frontend: Generated TypeScript Types
Pattern: Generate TypeScript types and paths from OpenAPI specs
Technology: openapi-typescript
What Gets Generated:
web-frontend/src/types/generated/
βββ company-api.types.ts # From companies-api.openapi.yml
βββ user-api.types.ts # From users-api.openapi.yml
βββ events-api.types.ts # From events-api.openapi.yml
βββ partners-api.types.ts # From partners-api.openapi.yml (future)
Package.json Scripts:
{
"scripts": {
"generate:api-types": "npm run generate:api-types:companies && npm run generate:api-types:users && npm run generate:api-types:events",
"generate:api-types:companies": "openapi-typescript ../docs/api/companies-api.openapi.yml -o src/types/generated/company-api.types.ts && prettier --write src/types/generated/company-api.types.ts",
"generate:api-types:users": "openapi-typescript ../docs/api/users-api.openapi.yml -o src/types/generated/user-api.types.ts && prettier --write src/types/generated/user-api.types.ts",
"generate:api-types:events": "openapi-typescript ../docs/api/events-api.openapi.yml -o src/types/generated/events-api.types.ts && prettier --write src/types/generated/events-api.types.ts"
}
}
Generated Types Example:
// src/types/generated/user-api.types.ts (auto-generated)
export interface paths {
"/api/v1/users/me": {
get: {
parameters: {
query?: {
include?: string;
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["UserResponse"];
};
};
404: {
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
patch: {
requestBody: {
content: {
"application/json": components["schemas"]["UpdateUserRequest"];
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["UserResponse"];
};
};
};
};
};
}
export interface components {
schemas: {
UserResponse: {
id: string;
email: string;
firstName?: string;
lastName?: string;
bio?: string;
companyId?: string;
roles: ("ORGANIZER" | "SPEAKER" | "SPONSOR" | "ATTENDEE")[];
isActive: boolean;
createdAt: string;
updatedAt: string;
};
UpdateUserRequest: {
firstName?: string;
lastName?: string;
bio?: string;
};
ErrorResponse: {
timestamp: string;
path: string;
status: number;
error: string;
message: string;
correlationId?: string;
severity?: "ERROR" | "WARNING" | "INFO";
};
};
}
Frontend Service Layer Uses Generated Types:
// src/services/userService.ts
import type { components } from '@/types/generated/user-api.types';
import { apiClient } from '@/lib/apiClient';
type UserResponse = components['schemas']['UserResponse'];
type UpdateUserRequest = components['schemas']['UpdateUserRequest'];
export const userService = {
async getCurrentUser(include?: string): Promise<UserResponse> {
const response = await apiClient.get<UserResponse>('/api/v1/users/me', {
params: { include }
});
return response.data;
},
async updateCurrentUser(request: UpdateUserRequest): Promise<UserResponse> {
const response = await apiClient.patch<UserResponse>('/api/v1/users/me', request);
return response.data;
}
};
React Component Uses Service Layer:
// src/components/UserProfile.tsx
import { userService } from '@/services/userService';
import type { components } from '@/types/generated/user-api.types';
type UserResponse = components['schemas']['UserResponse'];
export const UserProfile: React.FC = () => {
const [user, setUser] = useState<UserResponse | null>(null);
const handleUpdate = async (data: components['schemas']['UpdateUserRequest']) => {
const updated = await userService.updateCurrentUser(data);
setUser(updated);
};
// Component implementation with full type safety
};
4. Shared-Kernel Integration
Principle: Common infrastructure types are defined once and reused everywhere.
Shared-Kernel Types (NOT generated from OpenAPI):
ch.batbern.shared.dto.ErrorResponse- Standard error responsech.batbern.shared.api.PaginationMetadata- Pagination metadatach.batbern.shared.dto.PaginatedResponse<T>- Generic paginated responsech.batbern.shared.api.FilterParser- MongoDB-style JSON filtersch.batbern.shared.api.SortParser- Sort string parsingch.batbern.shared.api.IncludeParser- Resource expansionch.batbern.shared.exception.*- Exception hierarchy
OpenAPI Spec References Shared-Kernel Types:
# docs/api/users-api.openapi.yml
components:
schemas:
ErrorResponse:
# Reference to shared-kernel type (imported, not generated)
type: object
description: "Standard error response from shared-kernel"
# Schema definition here for documentation, but code generation uses importMappings
PaginationMetadata:
# Reference to shared-kernel type (imported, not generated)
type: object
description: "Pagination metadata from shared-kernel"
PaginatedUserResponse:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/UserResponse'
pagination:
$ref: '#/components/schemas/PaginationMetadata' # Uses shared-kernel
Backend Uses Import Mappings:
importMappings = [
'ErrorResponse': 'ch.batbern.shared.dto.ErrorResponse',
'PaginationMetadata': 'ch.batbern.shared.api.PaginationMetadata'
]
Frontend Manually Imports Shared Types (if needed):
// For ErrorResponse, the generated type matches shared-kernel structure
// No additional imports needed - types generated from OpenAPI spec
5. Code Generation Workflow
Backend (Java/Spring):
- Update OpenAPI spec:
docs/api/{service}-api.openapi.yml - Build service:
./gradlew :services:{service}:build- Runs
openApiGeneratetask automatically - Generates interfaces in
build/generated/src/main/java/ - Compiles with existing controller implementations
- Runs
- Fix compilation errors (if interface changed)
- Run tests to verify contract compliance
Frontend (TypeScript):
- Update OpenAPI spec:
docs/api/{service}-api.openapi.yml(same file as backend) - Generate types:
npm run generate:api-types- Generates TypeScript types in
src/types/generated/ - Auto-formats with Prettier
- Generates TypeScript types in
- Fix TypeScript errors (if types changed)
- Commit generated types to Git (convention: keep generated types versioned)
Automatic Regeneration:
- Backend: Generated code in
build/generated/(NOT committed, auto-regenerated on build) - Frontend: Generated types in
src/types/generated/(committed to Git for IDE support)
6. Documentation Pattern
Each service maintains an OPENAPI-CODEGEN.md file documenting:
- What gets generated (API interfaces, DTOs)
- What gets imported from shared-kernel
- How to regenerate code
- Implementation patterns (controller, service layer)
- Benefits and anti-patterns
Example: services/company-user-management-service/OPENAPI-CODEGEN.md
Alternatives Considered
Alternative 1: Code-First with Annotations
Approach: Use Spring annotations (@RestController, @GetMapping) and generate OpenAPI from code
Pros:
- No build-time code generation
- Direct implementation without interfaces
Cons:
- β OpenAPI spec becomes secondary (not source of truth)
- β Frontend types generated from backend implementation (tight coupling)
- β No build-time contract enforcement
- β Spec can drift from implementation
- β Harder to design APIs before implementation
Rejected because: OpenAPI spec must drive development, not be a byproduct
Alternative 2: Manual DTOs + Manual OpenAPI
Approach: Write DTOs manually, maintain OpenAPI spec separately, hope they match
Pros:
- No tooling required
- Full control over DTO structure
Cons:
- β High maintenance burden (duplicate work)
- β No automatic type consistency
- β OpenAPI specs quickly become outdated
- β No build-time validation
- β Frontend types manually created or generated separately
Rejected because: Doesnβt scale, high risk of drift
Alternative 3: GraphQL
Approach: Use GraphQL schema as contract, generate types for frontend/backend
Pros:
- Strong type system
- Single query language
- Field-level fetching
Cons:
- β Additional complexity (GraphQL server + REST APIs)
- β Team lacks GraphQL expertise
- β RESTful patterns well-established in codebase
- β Harder to cache at CDN/API Gateway level
- β Bruno API contract tests built for REST
Rejected because: REST + OpenAPI is simpler and sufficient for our needs
Alternative 4: Full Code Generation (Controllers + Services)
Approach: Generate controllers and service stubs from OpenAPI
Pros:
- Maximum automation
- Less manual code
Cons:
- β Loss of control over business logic structure
- β Harder to integrate with shared-kernel patterns
- β Generated code quality often suboptimal
- β Difficult to customize for complex scenarios
- β Doesnβt fit DDD layered architecture
Rejected because: We need control over business logic, generated interfaces provide optimal balance
Consequences
Positive
β Type Safety Across Stack: Backend DTOs and frontend types guaranteed to match (same source)
β Build-Time Contract Enforcement: Compilation fails if implementation diverges from OpenAPI spec
β Single Source of Truth: OpenAPI spec drives both frontend and backend, eliminates drift
β Reduced Boilerplate: No manual DTO creation, interfaces auto-generated
β Better API Design: Designing OpenAPI spec first forces thinking about API contract before implementation
β Improved Testing: Bruno API contract tests can reference OpenAPI spec for validation
β Frontend Efficiency: Frontend developers know exact response shapes before backend is ready
β Documentation Always Current: OpenAPI spec is the implementation contract, canβt become stale
β IDE Support: Full autocomplete and type checking for API clients and DTOs
β Architecture Compliance: Seamless integration with shared-kernel infrastructure
Negative
β οΈ Learning Curve: Developers must understand OpenAPI spec syntax and code generation tools
β οΈ Build Complexity: Additional Gradle plugin and npm script configuration
β οΈ Generated Code Limitations: Sometimes generated code needs workarounds for complex scenarios
β οΈ Frontend Generated Types Must Be Committed: Increases repository size slightly (trade-off for IDE support)
β οΈ Breaking Changes Cascade: OpenAPI spec changes can cause compilation errors across frontend and backend (this is actually a feature, but requires coordination)
Mitigation Strategies
For Learning Curve:
- Comprehensive OPENAPI-CODEGEN.md documentation per service
- This ADR provides architecture-level guidance
- OpenAPI DTO Migration Guide provides step-by-step migration process
- Code examples in stories (e.g., Story 2.7, BAT-17 Topics API Migration)
For Build Complexity:
- Standardized Gradle configuration across services
- Automated code generation on build (no manual steps)
For Generated Code Limitations:
- Use hybrid approach (generated interfaces, manual implementation)
- Shared-kernel integration via importMappings
- Custom business logic in service layer
For Breaking Changes:
- OpenAPI spec changes reviewed carefully
- Bruno E2E tests catch API contract breaks
- Semantic versioning for major API changes
References
Migration Guide
IMPORTANT: For teams migrating existing APIs from manual DTOs to generated DTOs, see:
- OpenAPI DTO Migration Guide - Step-by-step migration process, field reconciliation, common pitfalls
This ADR documents the architectural patterns. The migration guide provides the practical step-by-step process.
Implementation Examples
- Backend Pattern:
services/company-user-management-service/OPENAPI-CODEGEN.md - Gradle Config:
services/company-user-management-service/build.gradle(openApiGenerate task) - Frontend Scripts:
web-frontend/package.json(generate:api-types scripts) - Story 2.7: Partner Coordination Service (comprehensive OpenAPI codegen example)
- Topics API Migration (BAT-17): Complete reference implementation following migration guide
OpenAPI Specifications
docs/api/users-api.openapi.yml- User Management APIdocs/api/companies-api.openapi.yml- Company Management APIdocs/api/events-api.openapi.yml- Event Management APIdocs/api/partners-api.openapi.yml- Partner Coordination API (Story 2.7)
Tools
- Backend: OpenAPI Generator - Spring Boot 3 generator
- Frontend: openapi-typescript - TypeScript type generation
- Specification: OpenAPI 3.1
Related ADRs
- ADR-003: Meaningful Identifiers in Public APIs - Dual-identifier strategy complements OpenAPI contracts
- ADR-004: Factor User Fields from Domain Entities - HTTP enrichment pattern used in generated controllers
Builder Pattern for Generated DTOs
All OpenAPI generators are configured with generateBuilders: 'true' to enable fluent builder pattern for DTOs:
// Generated DTO with builder
TopicListResponse response = TopicListResponse.builder()
.data(topicDtos)
.pagination(paginationMetadata)
.build();
// Instead of manual setters
TopicListResponse response = new TopicListResponse();
response.setData(topicDtos);
response.setPagination(paginationMetadata);
Benefits:
- β Immutable object construction
- β Null safety (missing required fields caught at build time)
- β Better IDE autocomplete
- β More readable code (fluent API)
Revision History
| Version | Date | Changes | Author |
|---|---|---|---|
| 1.0 | 2025-01-08 | Initial ADR for OpenAPI contract-first code generation | Development Team |
| 1.1 | 2025-12-22 | Added Pure Mapper pattern, business logic separation, builder pattern configuration | Development Team |