Skip to main content

Code Generation and Templates

Overview

Code generation transforms machine-readable specifications (OpenAPI, GraphQL schemas, database schemas) into executable code. This eliminates the error-prone manual translation step where developers transcribe API contracts into DTOs, or database schemas into entity classes. The compiler enforces consistency - when the specification changes, the generated code changes, causing compilation errors in any code that depends on outdated assumptions. This makes breaking changes visible at build time rather than runtime.

Schema-first development inverts the traditional code-then-document workflow. Instead of writing implementation code first and documenting it afterward, you define the contract (API schema, database schema) as the authoritative source. Code generation then produces implementation scaffolding from this single source of truth. This approach prevents documentation drift (where docs and code diverge), enables parallel development (frontend and backend teams work from the same contract simultaneously), and catches integration issues through contract validation before any integration testing occurs.


Core Principles

  • Generate, Don't Handwrite: Repetitive code like DTOs, API clients, and database entities should be auto-generated from specifications
  • Single Source of Truth: The schema or contract is the authoritative definition; generated code is derived, not edited
  • Regenerate on Change: Specifications evolve; regenerating code ensures changes propagate to all consumers immediately
  • Version Control Generated Code: Committing generated files ensures team-wide consistency and enables code review of contract changes
  • Customize Through Configuration: Extend generators via configuration files or wrapper classes, never by editing generated code directly
  • Validate Specifications: Invalid specifications produce invalid code; validate schemas before generation to catch errors early

OpenAPI Code Generation

OpenAPI specifications define RESTful API contracts in YAML or JSON. These machine-readable contracts describe endpoints, HTTP methods, request/response structures, validation rules, and error responses. Generating code from OpenAPI specs ensures frontend and backend implement identical contracts - the compiler enforces this consistency.

This diagram illustrates schema-first development flow: a single OpenAPI specification generates both frontend client code and backend server interfaces. Changes to the specification propagate to both sides, causing compilation errors if either side doesn't implement the updated contract.

See OpenAPI Specifications for contract design best practices and API Contracts for contract-first vs code-first trade-offs.

Generating TypeScript API Clients

OpenAPI Generator parses YAML/JSON specifications and produces TypeScript interfaces (types representing request/response DTOs), enums (for status codes, categories), and fetch-based HTTP client methods (one method per API operation). The generated client provides compile-time type safety - if the API contract changes (e.g., a required field is added to PaymentRequest), the TypeScript compiler immediately flags every usage site with a compilation error.

Configuration:

// openapitools.json
{
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.2.0",
"generators": {
"payment-api": {
"generatorName": "typescript-fetch",
"output": "src/generated/api",
"inputSpec": "../backend/openapi/payment-api.yaml",
"additionalProperties": {
"typescriptThreePlus": true,
"supportsES6": true,
"withInterfaces": true,
"modelPropertyNaming": "camelCase",
"enumPropertyNaming": "UPPERCASE",
"npmName": "@bank/payment-api-client",
"npmVersion": "1.5.0"
}
}
}
}
}

The inputSpec points to the authoritative OpenAPI specification. The typescript-fetch generator produces modern TypeScript using the Fetch API. The modelPropertyNaming: "camelCase" option transforms snake_case API fields to camelCase TypeScript properties, matching JavaScript conventions.

Package.json Scripts:

{
"scripts": {
"generate:api": "openapi-generator-cli generate",
"generate:api:watch": "nodemon --watch ../backend/openapi --exec npm run generate:api",
"prebuild": "npm run generate:api",
"pretest": "npm run generate:api"
},
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2.7.0",
"nodemon": "^3.0.2"
}
}

The prebuild and pretest hooks regenerate clients before builds and tests, ensuring generated code stays synchronized with the specification. The watch script enables real-time regeneration during development - specification changes immediately update generated code.

Generated Structure:

src/generated/api/
├── apis/
│ └── PaymentsApi.ts # API client class with typed methods
├── models/
│ ├── PaymentRequest.ts # Request DTO interface
│ ├── PaymentResponse.ts # Response DTO interface
│ ├── PaymentStatus.ts # Enum for payment states
│ └── index.ts
├── runtime.ts # Base runtime (fetch wrapper, error handling)
└── index.ts # Barrel export

The apis/ directory contains client classes with methods like createPayment(). The models/ directory contains TypeScript interfaces representing API data structures. The runtime.ts file provides shared HTTP logic (authentication, error handling, request/response transformations).

Generating Spring Boot Server Stubs

While Springdoc enables code-first OpenAPI generation, contract-first development generates Spring Boot controller interfaces from OpenAPI specifications. The generator creates controller interfaces annotated with Spring's @RestController, @GetMapping, @PostMapping, etc. You implement these interfaces in concrete classes. This ensures implementation matches the contract exactly - if your implementation signature doesn't match the generated interface, the Java compiler reports an error.

Gradle Configuration:

// build.gradle
plugins {
id "org.openapi.generator" version "7.2.0"
}

openApiGenerate {
generatorName = "spring"
inputSpec = "$rootDir/src/main/resources/openapi/payment-api.yaml"
outputDir = "$buildDir/generated"
apiPackage = "com.bank.payments.api"
modelPackage = "com.bank.payments.model"
configOptions = [
interfaceOnly: "true",
useSpringBoot3: "true",
useTags: "true",
delegatePattern: "true",
performBeanValidation: "true",
useBeanValidation: "true"
]
}

sourceSets {
main {
java {
srcDir "$buildDir/generated/src/main/java"
}
}
}

compileJava.dependsOn tasks.openApiGenerate

The interfaceOnly: "true" option generates only interfaces, not implementations - you write the implementation logic. useBeanValidation: "true" adds @Valid, @NotNull, etc., based on the OpenAPI schema's validation constraints. compileJava.dependsOn tasks.openApiGenerate ensures code generation runs before compilation.

Implementing Generated Interfaces:

// Generated interface (do not edit)
@Generated("org.openapitools.codegen.languages.SpringCodegen")
@Validated
@Tag(name = "payments", description = "Payment operations")
public interface PaymentsApi {

@Operation(summary = "Create payment")
@ApiResponses(...)
@PostMapping(value = "/payments", produces = "application/json")
ResponseEntity<PaymentResponse> createPayment(
@Valid @RequestBody PaymentRequest request
);
}

// Your implementation
@RestController
@RequestMapping("/api/v1")
public class PaymentController implements PaymentsApi {

private final PaymentService paymentService;

public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}

@Override
public ResponseEntity<PaymentResponse> createPayment(PaymentRequest request) {
PaymentResponse response = paymentService.processPayment(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}

The generated PaymentsApi interface declares method signatures with Spring annotations. Your PaymentController implements this interface. If the OpenAPI spec changes (e.g., a new required parameter is added), the interface changes, and your implementation fails to compile until you update it.

See Frontend API Integration for using generated clients with React Query.


Database Migration Generation

Database migrations define incremental, versioned schema changes. Unlike JPA's ddl-auto, migrations provide:

  • Version Control: Each migration is a numbered file tracked in Git
  • Auditability: Database history is visible in migration files
  • Rollback Support: Migrations can define undo operations
  • Multi-environment Consistency: The same migrations apply to dev, staging, and production

This diagram shows sequential migration application. Flyway applies V1, records it in flyway_schema_history, then applies V2, etc. If V3 was already applied to production, Flyway skips it and applies only V4 when deploying.

See Database Migrations for comprehensive migration patterns, zero-downtime strategies, and rollback procedures, as well as Database Design for table design and indexing strategies.

Flyway SQL Migrations

Flyway executes versioned SQL scripts sequentially. It tracks applied migrations in a flyway_schema_history table. Migrations are immutable - once applied to production, never modify them. If you need to change a column, create a new migration that adds/modifies the column rather than editing the original migration.

Migration File Structure:

src/main/resources/db/migration/
├── V1__create_payment_tables.sql
├── V2__add_payment_status_index.sql
├── V3__add_audit_columns.sql
└── V4__add_currency_support.sql

Naming Convention: V<version>__<description>.sql

  • Version: Integer or dotted (e.g., V1, V1_1, V2_0_1) - Flyway sorts numerically
  • Double underscore __ separates version from description (single underscore in version, double before description)
  • Description: Snake_case words describing the change

Example Migration:

-- V1__create_payment_tables.sql
CREATE TABLE payments (
transaction_id UUID PRIMARY KEY,
amount DECIMAL(19, 2) NOT NULL,
currency VARCHAR(3) NOT NULL,
recipient VARCHAR(255) NOT NULL,
reference VARCHAR(500),
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP,
CONSTRAINT chk_amount_positive CHECK (amount > 0),
CONSTRAINT chk_currency_valid CHECK (currency IN ('USD', 'EUR', 'GBP', 'JPY', 'CAD'))
);

CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_created_at ON payments(created_at DESC);

-- Audit table for payment changes
CREATE TABLE payment_audit (
id BIGSERIAL PRIMARY KEY,
transaction_id UUID NOT NULL,
changed_by VARCHAR(255) NOT NULL,
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
old_status VARCHAR(20),
new_status VARCHAR(20),
change_reason TEXT,
FOREIGN KEY (transaction_id) REFERENCES payments(transaction_id)
);

This migration creates two tables atomically. The CHECK constraints enforce business rules at the database level (amounts must be positive, currencies must be valid). The indexes optimize common queries (filtering by status, sorting by creation date). The audit table tracks payment state changes - critical for financial applications requiring audit trails.

Gradle Configuration:

// build.gradle
plugins {
id 'org.flywaydb.flyway' version '10.4.1'
}

dependencies {
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'
}

flyway {
url = 'jdbc:postgresql://localhost:5432/payments_db'
user = 'db_user'
password = System.getenv('DB_PASSWORD')
locations = ['classpath:db/migration']
baselineOnMigrate = true
validateOnMigrate = true
}

The baselineOnMigrate: true setting handles existing databases - Flyway creates a baseline entry in flyway_schema_history for the current schema. validateOnMigrate: true validates checksums to detect modified migrations (preventing accidental changes to applied migrations).

Running Migrations:

# Apply pending migrations
./gradlew flywayMigrate

# Validate migrations (check for modified files)
./gradlew flywayValidate

# View migration status
./gradlew flywayInfo

# Repair checksums (use carefully after fixing migration files)
./gradlew flywayRepair

The flywayInfo command shows applied migrations, pending migrations, and their states. Use flywayRepair only to fix checksum mismatches (e.g., after fixing line endings), never to modify applied migrations.

Spring Boot Integration:

# application.yml
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
out-of-order: false
clean-disabled: true # Never allow clean in production!

Spring Boot automatically runs Flyway migrations on startup. The clean-disabled: true setting prevents accidental flyway clean commands (which drop all database objects) in production. out-of-order: false enforces sequential migration application - if V5 is pending but V6 is applied, Flyway fails rather than applying V5 out of sequence.

Production Migration Safety

Never modify applied migrations - create new ones instead. Test migrations on staging first (with production-like data volume). Backup the database before applying migrations. Make migrations reversible (write a corresponding down migration). Disable clean in production (it drops all objects). Use transactions where possible (BEGIN; ... COMMIT;), though DDL statements auto-commit in some databases.

Liquibase XML/YAML Migrations

Liquibase uses database-agnostic XML/YAML changesets. Unlike Flyway's SQL (which uses database-specific syntax), Liquibase abstracts database operations. At runtime, Liquibase translates changesets to the target database's SQL dialect. This enables the same migration file to work across PostgreSQL, MySQL, Oracle, and SQL Server. Liquibase tracks changes in DATABASECHANGELOG with checksums to detect modifications. Built-in rollback support contrasts with Flyway's manual rollback scripts.

Changeset Structure:

# src/main/resources/db/changelog/db.changelog-master.yaml
databaseChangeLog:
- include:
file: db/changelog/changes/v1.0-create-tables.yaml
- include:
file: db/changelog/changes/v1.1-add-indexes.yaml
- include:
file: db/changelog/changes/v1.2-add-audit-columns.yaml

The master changelog includes individual change files. This modular structure organizes migrations by feature or version. Liquibase applies changes from all included files sequentially.

Example Changeset:

# src/main/resources/db/changelog/changes/v1.0-create-tables.yaml
databaseChangeLog:
- changeSet:
id: 1
author: john.doe
comment: Create payments table
changes:
- createTable:
tableName: payments
columns:
- column:
name: transaction_id
type: UUID
constraints:
primaryKey: true
nullable: false
- column:
name: amount
type: DECIMAL(19, 2)
constraints:
nullable: false
- column:
name: currency
type: VARCHAR(3)
constraints:
nullable: false
- column:
name: status
type: VARCHAR(20)
constraints:
nullable: false
- column:
name: created_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
constraints:
nullable: false
rollback:
- dropTable:
tableName: payments

- changeSet:
id: 2
author: john.doe
comment: Add indexes on payments table
changes:
- createIndex:
indexName: idx_payments_status
tableName: payments
columns:
- column:
name: status
- createIndex:
indexName: idx_payments_created_at
tableName: payments
columns:
- column:
name: created_at
descending: true
rollback:
- dropIndex:
indexName: idx_payments_status
tableName: payments
- dropIndex:
indexName: idx_payments_created_at
tableName: payments

Each changeSet has a unique id (within the file) and author. The rollback section defines how to undo the change. Liquibase automatically generates rollback SQL for simple operations (like createTable), but explicit rollback definitions handle complex scenarios.

Gradle Configuration:

// build.gradle
plugins {
id 'org.liquibase.gradle' version '2.2.0'
}

dependencies {
implementation 'org.liquibase:liquibase-core:4.25.0'
liquibaseRuntime 'org.liquibase:liquibase-core:4.25.0'
liquibaseRuntime 'org.postgresql:postgresql'
liquibaseRuntime 'info.picocli:picocli:4.7.5'
}

liquibase {
activities {
main {
changelogFile 'src/main/resources/db/changelog/db.changelog-master.yaml'
url 'jdbc:postgresql://localhost:5432/payments_db'
username 'db_user'
password System.getenv('DB_PASSWORD')
}
}
}

Running Migrations:

# Apply pending changesets
./gradlew update

# Generate SQL preview (don't apply)
./gradlew updateSQL

# Rollback last changeset
./gradlew rollbackCount -PliquibaseCommandValue=1

# Generate diff between database and changelog
./gradlew diff

The updateSQL command generates SQL without executing it - use this to review changes before applying. The diff command compares the current database schema to the changelog, identifying drift (manual schema changes bypassing migrations).

Liquibase vs Flyway:

FeatureFlywayLiquibase
Migration FormatSQL (native dialect)XML/YAML (database-agnostic)
RollbackManual SQL scriptsBuilt-in rollback support
Database SupportMost SQL databasesMost SQL databases + NoSQL
Learning CurveSimple (just SQL)Steeper (XML/YAML syntax)
Diff GenerationNoYes (can diff database vs changelog)
Conditional LogicVia SQLVia preconditions
Best ForSingle-database projects (e.g., PostgreSQL-only)Multi-database projects requiring portability

Choose Flyway for PostgreSQL-only projects where SQL readability is prioritized. Choose Liquibase for multi-database environments or when automatic rollback generation is critical.

JPA Schema Generation (Development Only)

JPA generates database schema from entity annotations. Hibernate inspects @Entity classes and creates tables, columns, indexes, and foreign keys. This accelerates local development - no manual migration writing. Never use in production - use JPA-generated DDL as a starting point for Flyway/Liquibase migrations.

# application-dev.yml (development only!)
spring:
jpa:
hibernate:
ddl-auto: update # Creates/updates tables based on entities
properties:
hibernate:
format_sql: true
show_sql: true
# Generate DDL files
properties:
javax.persistence.schema-generation:
scripts:
action: create
create-target: target/generated-ddl.sql

The ddl-auto: update setting creates missing tables/columns and updates existing ones. The create-target setting writes generated DDL to a file for review.

Generate DDL from Entities:

@Entity
@Table(name = "payments")
public class Payment {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "transaction_id")
private UUID transactionId;

@Column(nullable = false, precision = 19, scale = 2)
private BigDecimal amount;

@Column(nullable = false, length = 3)
private String currency;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentStatus status;

@CreatedDate
@Column(nullable = false, updatable = false)
private Instant createdAt;

@LastModifiedDate
@Column(nullable = false)
private Instant updatedAt;

// Getters, setters, equals, hashCode
}

JPA reads annotations and generates: CREATE TABLE payments (transaction_id UUID PRIMARY KEY, amount DECIMAL(19,2) NOT NULL, ...). See Spring Boot Data for JPA entity design patterns.

Workflow for Production Migrations:

  1. Use ddl-auto: update in local development
  2. Let JPA generate DDL to target/generated-ddl.sql
  3. Review generated SQL (add indexes, constraints, custom types)
  4. Create proper Flyway migration from generated SQL
  5. Test migration on local database
  6. Commit migration to version control
  7. Apply to staging/production via Flyway
Never Use JPA Schema Generation in Production

JPA schema generation bypasses version control (changes are untracked), is non-deterministic (depends on entity scanning order), provides no rollback mechanism, risks data loss (create-drop destroys data on shutdown), and bypasses code review. Use Flyway/Liquibase for all environments except local development.


GraphQL Code Generation

GraphQL schemas define type-safe query APIs. Unlike REST's endpoint-based contracts, GraphQL uses a single endpoint with a typed schema. Generating code from GraphQL schemas ensures type safety between client queries and server resolvers - the compiler enforces that queries only request fields defined in the schema.

This diagram shows schema-first GraphQL development. A single schema generates TypeScript types for frontend queries and Java types for backend resolvers, ensuring both sides agree on the data structure.

See GraphQL API Design for schema design, resolver optimization, and performance monitoring.

GraphQL Code Generation for TypeScript

GraphQL Code Generator parses schemas and queries to generate TypeScript interfaces, operation types, and framework-specific hooks (Apollo, urql). Generated hooks provide compile-time type safety - if you add a field to a query (createdBy), but the schema doesn't define it, TypeScript compilation fails immediately.

Installation:

npm install --save-dev @graphql-codegen/cli
npm install --save-dev @graphql-codegen/typescript
npm install --save-dev @graphql-codegen/typescript-operations
npm install --save-dev @graphql-codegen/typescript-react-apollo

Configuration:

# codegen.yml
overwrite: true
schema: "http://localhost:8080/graphql" # Schema source (introspection)
documents: "src/**/*.graphql" # Query/mutation files
generates:
src/generated/graphql.ts:
plugins:
- typescript # Generate base types
- typescript-operations # Generate operation types
- typescript-react-apollo # Generate React hooks
config:
withHooks: true
withComponent: false
withHOC: false
scalars:
UUID: string
DateTime: string
Decimal: number

The schema can be a URL (introspection), a local .graphql file, or a glob pattern. The documents glob finds all .graphql files containing queries/mutations. The scalars mapping defines TypeScript types for custom scalars (GraphQL doesn't know UUID maps to JavaScript string).

GraphQL Schema Example:

# schema.graphql
type Payment {
transactionId: UUID!
amount: Decimal!
currency: Currency!
recipient: String!
reference: String
status: PaymentStatus!
createdAt: DateTime!
updatedAt: DateTime!
}

enum Currency {
USD
EUR
GBP
JPY
CAD
}

enum PaymentStatus {
PENDING
PROCESSING
COMPLETED
FAILED
CANCELLED
}

input CreatePaymentInput {
amount: Decimal!
currency: Currency!
recipient: String!
reference: String
}

type Query {
payment(transactionId: UUID!): Payment
payments(status: PaymentStatus, limit: Int = 20, offset: Int = 0): [Payment!]!
}

type Mutation {
createPayment(input: CreatePaymentInput!): Payment!
cancelPayment(transactionId: UUID!): Payment!
}

This schema defines types (Payment), enums (Currency, PaymentStatus), inputs (CreatePaymentInput), queries (payment, payments), and mutations (createPayment, cancelPayment). The ! denotes non-null fields.

GraphQL Query Files:

# src/queries/payments.graphql
query GetPayment($transactionId: UUID!) {
payment(transactionId: $transactionId) {
transactionId
amount
currency
recipient
reference
status
createdAt
}
}

mutation CreatePayment($input: CreatePaymentInput!) {
createPayment(input: $input) {
transactionId
amount
currency
status
createdAt
}
}

Each query/mutation file defines operations. The generator parses these to create typed hooks - useGetPaymentQuery, useCreatePaymentMutation.

Generated TypeScript Types:

// src/generated/graphql.ts (generated, do not edit)
export type Maybe<T> = T | null;
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
UUID: string;
DateTime: string;
Decimal: number;
};

export enum Currency {
Usd = 'USD',
Eur = 'EUR',
Gbp = 'GBP',
Jpy = 'JPY',
Cad = 'CAD'
}

export enum PaymentStatus {
Pending = 'PENDING',
Processing = 'PROCESSING',
Completed = 'COMPLETED',
Failed = 'FAILED',
Cancelled = 'CANCELLED'
}

export type Payment = {
__typename?: 'Payment';
transactionId: Scalars['UUID'];
amount: Scalars['Decimal'];
currency: Currency;
recipient: Scalars['String'];
reference?: Maybe<Scalars['String']>;
status: PaymentStatus;
createdAt: Scalars['DateTime'];
};

// Generated hooks
export function useGetPaymentQuery(
baseOptions: Apollo.QueryHookOptions<GetPaymentQuery, GetPaymentQueryVariables>
) {
return Apollo.useQuery<GetPaymentQuery, GetPaymentQueryVariables>(GetPaymentDocument, baseOptions);
}

export function useCreatePaymentMutation(
baseOptions?: Apollo.MutationHookOptions<CreatePaymentMutation, CreatePaymentMutationVariables>
) {
return Apollo.useMutation<CreatePaymentMutation, CreatePaymentMutationVariables>(CreatePaymentDocument, baseOptions);
}

The generator produces enums (Currency, PaymentStatus), types (Payment), and hooks (useGetPaymentQuery). The hooks are fully typed - TypeScript infers variable types, response shapes, and error types.

Using Generated Types:

// src/components/PaymentForm.tsx
import { useCreatePaymentMutation, Currency } from '../generated/graphql';

export function PaymentForm() {
const [createPayment, { data, loading, error }] = useCreatePaymentMutation();

const handleSubmit = async (formData: any) => {
// Generated types ensure type safety
const result = await createPayment({
variables: {
input: {
amount: formData.amount,
currency: Currency.Usd, // Enum from generated types
recipient: formData.recipient,
reference: formData.reference
}
}
});

// result.data.createPayment is fully typed
console.log('Payment created:', result.data?.createPayment?.transactionId);
};

return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}

The useCreatePaymentMutation hook returns typed data, loading, error. The variables object is type-checked - if you pass an invalid currency or omit a required field, TypeScript compilation fails.

Scripts:

{
"scripts": {
"generate:graphql": "graphql-codegen --config codegen.yml",
"generate:graphql:watch": "graphql-codegen --config codegen.yml --watch",
"prebuild": "npm run generate:graphql"
}
}

GraphQL Code Generation for Java

Generating Java resolvers and types from GraphQL schemas ensures backend type safety. DGS Codegen (Netflix) generates Java classes from GraphQL schemas, including types, inputs, enums, and resolver scaffolding.

Gradle Configuration (DGS Codegen):

// build.gradle
plugins {
id 'com.netflix.dgs.codegen' version '6.0.3'
}

dependencies {
implementation 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:latest.release'
}

generateJava {
schemaPaths = ["${projectDir}/src/main/resources/schema"]
packageName = 'com.bank.payments.graphql.generated'
generateClient = true
generateDataTypes = true
typeMapping = [
'UUID': 'java.util.UUID',
'DateTime': 'java.time.Instant',
'Decimal': 'java.math.BigDecimal'
]
}

The typeMapping defines Java types for custom GraphQL scalars. The generateDataTypes: true option creates POJOs for GraphQL types.

Generated Java Types:

// Generated by DGS Codegen (do not edit)
package com.bank.payments.graphql.generated.types;

public class Payment {
private UUID transactionId;
private BigDecimal amount;
private Currency currency;
private String recipient;
private String reference;
private PaymentStatus status;
private Instant createdAt;

// Getters, setters, builder
}

public enum Currency {
USD, EUR, GBP, JPY, CAD
}

public enum PaymentStatus {
PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED
}

public class CreatePaymentInput {
private BigDecimal amount;
private Currency currency;
private String recipient;
private String reference;

// Getters, setters, builder
}

Implementing Resolvers:

// Your implementation uses generated types
@DgsComponent
public class PaymentDataFetcher {

private final PaymentService paymentService;

@DgsQuery
public Payment payment(@InputArgument UUID transactionId) {
return paymentService.getPayment(transactionId)
.map(this::toGraphQLPayment)
.orElseThrow(() -> new PaymentNotFoundException(transactionId));
}

@DgsMutation
public Payment createPayment(@InputArgument CreatePaymentInput input) {
// Input types are generated and type-safe
PaymentRequest request = PaymentRequest.builder()
.amount(input.getAmount())
.currency(input.getCurrency())
.recipient(input.getRecipient())
.reference(input.getReference())
.build();

PaymentResponse response = paymentService.processPayment(request);
return toGraphQLPayment(response);
}

private Payment toGraphQLPayment(PaymentResponse response) {
return Payment.builder()
.transactionId(response.getTransactionId())
.amount(response.getAmount())
.currency(Currency.valueOf(response.getCurrency()))
.status(PaymentStatus.valueOf(response.getStatus().name()))
.createdAt(response.getCreatedAt())
.build();
}
}

The @DgsQuery and @DgsMutation annotations define resolvers. The @InputArgument annotation binds method parameters to GraphQL arguments. The generated CreatePaymentInput type ensures type safety - if the schema changes, the generated class changes, causing compilation errors in dependent code.


IDE Templates and Snippets

IDE templates expand abbreviations into common code patterns. Instead of typing boilerplate repeatedly, type an abbreviation (e.g., sbrc), press Tab, and the IDE expands it to a full REST controller. Templates reduce typing, enforce consistency (all controllers follow the same structure), and minimize typos. See IDE Setup for comprehensive template configuration and productivity features.

IntelliJ IDEA Live Templates

Live Templates expand abbreviations when you press Tab. They support variables (placeholders you fill in), expressions (dynamic values like filenames), and cursor positioning (where the cursor lands after expansion).

Accessing Live Templates: Settings > Editor > Live Templates

Creating Custom Templates:

<!-- .idea/templates/Java.xml -->
<templateSet group="Java">

<!-- Spring Boot REST Controller -->
<template name="sbrc" value="@RestController&#10;@RequestMapping(&quot;/api/v1/$PATH$&quot;)&#10;@RequiredArgsConstructor&#10;@Validated&#10;public class $NAME$ {&#10; &#10; private final $SERVICE$ $SERVICE_VAR$;&#10; &#10; $END$&#10;}&#10;" description="Spring Boot REST Controller" toReformat="true" toShortenFQNames="true">
<variable name="PATH" expression="" defaultValue="&quot;payments&quot;" alwaysStopAt="true" />
<variable name="NAME" expression="" defaultValue="&quot;PaymentController&quot;" alwaysStopAt="true" />
<variable name="SERVICE" expression="" defaultValue="&quot;PaymentService&quot;" alwaysStopAt="true" />
<variable name="SERVICE_VAR" expression="decapitalize(SERVICE)" defaultValue="" alwaysStopAt="false" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>

<!-- JUnit 5 Test Class -->
<template name="jtest" value="@ExtendWith(MockitoExtension.class)&#10;class $NAME$ {&#10; &#10; @Mock&#10; private $DEPENDENCY$ $DEPENDENCY_VAR$;&#10; &#10; @InjectMocks&#10; private $CLASS_UNDER_TEST$ $CLASS_VAR$;&#10; &#10; @Test&#10; void $METHOD$() {&#10; $END$&#10; }&#10;}&#10;" description="JUnit 5 Test Class" toReformat="true" toShortenFQNames="true">
<variable name="NAME" expression="fileNameWithoutExtension() + &quot;Test&quot;" defaultValue="" alwaysStopAt="false" />
<variable name="DEPENDENCY" expression="" defaultValue="&quot;Repository&quot;" alwaysStopAt="true" />
<variable name="DEPENDENCY_VAR" expression="decapitalize(DEPENDENCY)" defaultValue="" alwaysStopAt="false" />
<variable name="CLASS_UNDER_TEST" expression="" defaultValue="&quot;Service&quot;" alwaysStopAt="true" />
<variable name="CLASS_VAR" expression="decapitalize(CLASS_UNDER_TEST)" defaultValue="" alwaysStopAt="false" />
<variable name="METHOD" expression="" defaultValue="&quot;shouldProcessPaymentSuccessfully&quot;" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>

</templateSet>

The $PATH$, $NAME$, $SERVICE$ are variables (placeholders). The decapitalize(SERVICE) expression transforms PaymentService to paymentService. The $END$ marker places the cursor after expansion.

Using Live Templates:

  1. Type abbreviation (e.g., sbrc)
  2. Press Tab
  3. Template expands with placeholders highlighted
  4. Fill in placeholders, press Tab to move between them
  5. Press Enter when done

Common Spring Boot Templates:

AbbreviationExpands ToDescription
sbrcREST Controller classSpring Boot REST controller with annotations
sbrm@GetMapping methodGET endpoint method
sbrp@PostMapping methodPOST endpoint method
jtestJUnit test classTest class with Mockito annotations
jmock@Mock fieldMock dependency field
jverifyverify(mock).method()Mockito verification
jassertassertThat(...).isEqualTo(...)AssertJ assertion

VS Code Snippets

VS Code snippets work similarly to Live Templates but use JSON format. Snippets support placeholders (${1:name}), choices (${1|option1,option2|}), and dynamic values ($TM_FILENAME).

Creating Snippets: File > Preferences > Configure User Snippets

TypeScript React Snippets:

// typescript.json
{
"React Functional Component": {
"prefix": "rfc",
"body": [
"import React from 'react';",
"",
"interface ${1:ComponentName}Props {",
" ${2:prop}: ${3:string};",
"}",
"",
"export function ${1:ComponentName}({ ${2:prop} }: ${1:ComponentName}Props) {",
" return (",
" <div>",
" $0",
" </div>",
" );",
"}",
""
],
"description": "React functional component with TypeScript"
},

"React Query Hook": {
"prefix": "usequery",
"body": [
"export function use${1:ResourceName}(${2:id}: string) {",
" return useQuery({",
" queryKey: ['${3:resource}', ${2:id}],",
" queryFn: () => ${4:apiClient}.get${1:ResourceName}(${2:id}),",
" enabled: !!${2:id}",
" });",
"}",
"$0"
],
"description": "React Query hook"
},

"React Mutation Hook": {
"prefix": "usemutation",
"body": [
"export function use${1:Action}${2:ResourceName}() {",
" const queryClient = useQueryClient();",
" ",
" return useMutation({",
" mutationFn: (${3:data}: ${4:InputType}) => ${5:apiClient}.${6:action}${2:ResourceName}(${3:data}),",
" onSuccess: () => {",
" queryClient.invalidateQueries({ queryKey: ['${7:resource}'] });",
" }",
" });",
"}",
"$0"
],
"description": "React Query mutation hook"
}
}

The ${1:ComponentName} is a placeholder with default value ComponentName. The $0 is the final cursor position.

Using VS Code Snippets:

  1. Type prefix (e.g., rfc)
  2. Select snippet from autocomplete
  3. Fill in placeholders (Tab to move between them)
  4. Template expands to full code

Sharing Snippets Across Team:

// .vscode/typescript.code-snippets (committed to repo)
{
"Payment Form Component": {
"prefix": "payment-form",
"scope": "typescript,typescriptreact",
"body": [
"import { useForm } from 'react-hook-form';",
"import { useCreatePayment } from '@/hooks/usePayments';",
"import { PaymentRequest } from '@/generated/api';",
"",
"export function PaymentForm() {",
" const { register, handleSubmit, formState: { errors } } = useForm<PaymentRequest>();",
" const createPayment = useCreatePayment();",
" ",
" const onSubmit = async (data: PaymentRequest) => {",
" await createPayment.mutateAsync(data);",
" };",
" ",
" return (",
" <form onSubmit={handleSubmit(onSubmit)}>",
" $0",
" </form>",
" );",
"}"
],
"description": "Payment form component with validation"
}
}

Committing .vscode/ snippets to the repo shares them team-wide. New team members automatically get project-specific snippets when they clone the repository.


Project Scaffolding Tools

Scaffolding tools generate complete project structures with dependencies, configuration, and boilerplate already in place. This eliminates manual setup (creating directories, configuring Gradle, adding dependencies) and ensures projects start with best practices.

Spring Initializr

Spring Initializr generates Spring Boot projects with selected dependencies, Gradle/Maven configuration, and package structure.

Using Web Interface:

  1. Navigate to start.spring.io
  2. Select options:
    • Project: Gradle - Groovy
    • Language: Java
    • Spring Boot: 3.2.x (latest stable)
    • Project Metadata: Group (com.bank), Artifact (payment-service), Name, Package
    • Packaging: Jar
    • Java: 21
  3. Add dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver, Validation, Flyway, Springdoc OpenAPI, Actuator
  4. Click "Generate" to download ZIP

Using Command Line:

# Using curl
curl https://start.spring.io/starter.zip \
-d dependencies=web,data-jpa,postgresql,validation,flyway,actuator,docker-compose \
-d bootVersion=3.4.1 \
-d javaVersion=25 \
-d type=gradle-project \
-d groupId=com.bank \
-d artifactId=payment-service \
-d name=payment-service \
-d packageName=com.bank.payments \
-o payment-service.zip

unzip payment-service.zip
cd payment-service

The -d dependencies=... flag specifies Spring Boot starters. The -d type=gradle-project flag selects Gradle over Maven.

Using Spring CLI:

# Install Spring CLI
sdk install springboot

# Generate project
spring init \
--dependencies=web,data-jpa,postgresql,validation,flyway \
--build=gradle \
--java-version=21 \
--group-id=com.bank \
--artifact-id=payment-service \
--name=payment-service \
--package-name=com.bank.payments \
payment-service

Customizing Generated Project:

After generation, customize:

  1. Update application.yml with environment-specific configuration (database URL, pool size, Flyway settings)
  2. Configure Gradle plugins (Spotless for formatting, JaCoCo for coverage, PITest for mutation testing, Docker for containerization)
  3. Add OpenAPI specification to src/main/resources/openapi/
  4. Create database migrations in src/main/resources/db/migration/
  5. Set up CI/CD pipeline (.gitlab-ci.yml for GitLab) - see Pipeline Configuration
  6. Add README with project documentation

Create React App / Vite

Vite is the recommended tool for React projects. Vite provides faster development server startup (ES modules instead of bundling), faster hot module replacement (HMR), and smaller production bundles than Create React App (CRA). CRA is in maintenance mode; new projects should use Vite.

Creating React + TypeScript Project:

# Using Vite (recommended)
npm create vite@latest payment-app -- --template react-ts

cd payment-app
npm install

# Add common dependencies
npm install @tanstack/react-query @tanstack/react-query-devtools
npm install react-router-dom
npm install zustand
npm install react-hook-form
npm install axios

# Add dev dependencies
npm install --save-dev @types/node
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm install --save-dev prettier eslint-config-prettier
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install --save-dev vitest @vitest/ui

The --template react-ts flag generates a TypeScript React project. React Query provides data fetching, Zustand manages global state, React Hook Form handles forms, and Vitest runs tests.

Project Structure After Setup:

payment-app/
├── src/
│ ├── components/ # React components
│ ├── hooks/ # Custom hooks (React Query, Zustand)
│ ├── services/ # API services
│ ├── generated/ # Generated API clients (from OpenAPI)
│ ├── store/ # Zustand stores
│ ├── types/ # TypeScript types
│ ├── utils/ # Utility functions
│ ├── App.tsx
│ └── main.tsx
├── public/
├── tests/
├── .eslintrc.json
├── .prettierrc
├── tsconfig.json
├── vite.config.ts
└── package.json

Angular CLI

Angular CLI scaffolds Angular applications with routing, modules, components, services, and testing infrastructure.

# Install Angular CLI
npm install -g @angular/cli@17

# Create new Angular project
ng new payment-app \
--routing \
--style=scss \
--strict

cd payment-app

# Generate module with routing
ng generate module features/payments --routing

# Generate component
ng generate component features/payments/components/payment-form

# Generate service
ng generate service features/payments/services/payment

# Generate interface
ng generate interface features/payments/models/payment

# Generate guard
ng generate guard core/guards/auth

The --routing flag adds Angular Router. The --strict flag enables strict TypeScript checking. The CLI generates TypeScript files, HTML templates, SCSS stylesheets, and Jest specs.

Customization:

// angular.json - Configure linting, testing, building
{
"projects": {
"payment-app": {
"architect": {
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
}
}
}
}
}

React Native CLI

React Native CLI scaffolds mobile applications for iOS and Android.

# Create React Native project
npx react-native@latest init PaymentApp --template react-native-template-typescript

cd PaymentApp

# Install common dependencies
npm install @react-navigation/native @react-navigation/stack
npm install react-native-screens react-native-safe-area-context
npm install @tanstack/react-query
npm install zustand
npm install axios

# iOS specific
cd ios && pod install && cd ..

# Run on iOS
npm run ios

# Run on Android
npm run android

The --template react-native-template-typescript flag generates a TypeScript project. React Navigation provides navigation, React Query handles data fetching, and Zustand manages state.


Anti-Patterns

Editing Generated Code Directly

Bad:

// src/generated/api/PaymentsApi.ts (generated)
export class PaymentsApi {
// Manually added method (will be lost on regeneration!)
async retryPayment(paymentId: string) {
// custom logic
}
}

Manual edits to generated files are lost when regeneration occurs. Generated files are marked with @generated comments warning against manual edits.

Good:

// src/services/PaymentApiWrapper.ts
import { PaymentsApi, Configuration } from '../generated/api';

export class PaymentApiWrapper extends PaymentsApi {

constructor(config: Configuration) {
super(config);
}

// Add custom methods here
async retryPayment(paymentId: string) {
// Call generated method with retry logic
return this.createPayment({ paymentRequest: { /* retry data */ } });
}

// Override generated method if needed
async createPayment(params: any) {
console.log('Creating payment:', params);
return super.createPayment(params);
}
}

Wrapper classes extend generated code without modifying it. Regeneration doesn't affect custom logic.

Ignoring Generated Files in Version Control

Bad:

# .gitignore
src/generated/ # Don't commit generated code

Ignoring generated files forces every developer to regenerate code on checkout. Differences in generator versions, configurations, or timing cause inconsistent generated code across the team.

Good:

Commit generated code to version control. This ensures:

  • Everyone uses identical generated code
  • Code reviews show API contract changes
  • No build-time generation delays
  • Deterministic builds

Alternative for Large Teams: Publish generated code as npm packages:

// package.json for generated client
{
"name": "@bank/payment-api-client",
"version": "1.5.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"generate": "openapi-generator-cli generate",
"build": "npm run generate && tsc",
"prepublishOnly": "npm run build"
}
}

Consumer projects install the package (npm install @bank/payment-api-client), avoiding generation entirely.

Modifying Applied Migrations

Bad:

-- V1__create_payment_tables.sql (already applied to production)
-- Developer edits file to add new column
CREATE TABLE payments (
transaction_id UUID PRIMARY KEY,
amount DECIMAL(19, 2) NOT NULL,
new_column VARCHAR(255) -- Added after production deployment
);

Flyway checksums detect modifications to applied migrations and fails. Even if you use flywayRepair to fix checksums, production databases won't receive the change (migration already applied).

Good:

-- V2__add_new_column_to_payments.sql (new migration)
ALTER TABLE payments ADD COLUMN new_column VARCHAR(255);

Create new migrations for changes. This maintains migration history and applies changes to all environments.

Using JPA Schema Generation in Production

Bad:

# application-prod.yml (production)
spring:
jpa:
hibernate:
ddl-auto: update # Dangerous in production!

JPA schema generation bypasses version control, code review, and rollback mechanisms. Schema changes are untracked. Data loss occurs with create-drop.

Good:

# application-prod.yml
spring:
jpa:
hibernate:
ddl-auto: validate # Only validates schema matches entities
flyway:
enabled: true

Use Flyway/Liquibase for production. Use JPA's validate mode to ensure entities match the schema (migration-created).

Not Formatting Generated Code

Bad:

Generated code uses different formatting (tabs vs spaces, line length) than team standards, causing linter failures and cluttered diffs.

Good:

{
"scripts": {
"generate:api": "openapi-generator-cli generate && npm run format:generated",
"format:generated": "prettier --write 'src/generated/**/*.ts'"
}
}

Post-generation formatting ensures consistency. Alternatively, configure ESLint to ignore generated files:

// .eslintrc.json
{
"ignorePatterns": ["src/generated/**/*.ts"]
}

Best Practices

Validate Specifications Before Generation

Invalid specifications produce invalid code. Validate OpenAPI specs with openapi-generator-cli validate or swagger-cli validate. Validate GraphQL schemas with graphql-schema-linter.

{
"scripts": {
"validate:openapi": "swagger-cli validate ../backend/openapi/payment-api.yaml",
"pregenerate:api": "npm run validate:openapi"
}
}

Automate Generation in CI/CD

CI pipelines should regenerate code and compare with committed versions. If generated code differs, fail the build and instruct developers to regenerate locally.

# .gitlab-ci.yml
generate-api-clients:
stage: build
script:
- npm run generate:api
# Fail if generated code differs from committed version
- git diff --exit-code src/generated/ || (echo "Generated code changed! Run npm run generate:api locally." && exit 1)

This prevents committing outdated generated code.

Version Specifications with Code

OpenAPI specs, GraphQL schemas, and database migrations are code. Version them in Git alongside implementation code. Tag specification versions matching API versions.

git tag v1.5.0-api
git push --tags

Document Generation Process

README files should explain:

  • What gets generated
  • How to regenerate code (npm run generate:api)
  • Where specifications live (../backend/openapi/)
  • What to do when generation fails (validate spec, check generator version)
# README.md

## Code Generation

This project uses OpenAPI Generator to generate TypeScript API clients.

### Regenerating Clients

```bash
npm run generate:api

Updating API Contract

  1. Update ../backend/openapi/payment-api.yaml
  2. Run npm run generate:api
  3. Fix compilation errors in code using the API client
  4. Commit both the spec and generated code

### Use Generator Configuration, Not Manual Edits

Generators support extensive configuration. Use configuration files to customize output rather than editing generated code.

```json
// openapitools.json
{
"generator-cli": {
"generators": {
"payment-api": {
"additionalProperties": {
"typescriptThreePlus": true,
"withInterfaces": true,
"modelPropertyNaming": "camelCase"
},
"globalProperties": {
"modelDocs": "false",
"apiDocs": "false"
}
}
}
}
}

Further Reading

Internal Documentation

External Resources