Skip to content

Dev Guides

Tiana_ edited this page May 30, 2026 · 1 revision

Dev Guides - Coding Standards, Dependencies, Git Flow, Release Flow, ADR Process

Operational practices for contributors. Index of dev-related Wiki pages plus consolidated guides for short topics.

Topic Page
Kotlin best practices Code-Rules
Testing Testing-Strategy
Threat modeling Threat-Modeling
Project goals & roadmap Roadmap
Coding standards (formatter) this page §1
Dependency management this page §2
Git flow this page §3
Release flow this page §4
ADR process this page §5

§1. Coding Standards (formatter setup)

Tools

Tool Purpose Config file
ktlint Default Kotlin style ruleset .editorconfig
detekt Static analysis: complexity, magic numbers, dead code detekt.yml
Spotless Pre-commit auto-fix orchestration build.gradle.kts
EditorConfig Indent, line endings .editorconfig

.editorconfig

root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.kt]
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^

[*.{yml,yaml}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false

detekt.yml (excerpts)

complexity:
  CyclomaticComplexMethod:
    threshold: 15
  LongMethod:
    threshold: 30
  LongParameterList:
    functionThreshold: 6
    constructorThreshold: 6
  TooManyFunctions:
    thresholdInClasses: 11

style:
  MagicNumber:
    ignoreNumbers: ['-1', '0', '1', '2', '100']
  ReturnCount:
    max: 3

empty-blocks:
  EmptyCatchBlock: error

Spotless wiring

plugins {
    id("com.diffplug.spotless") version "7.0.4"
}

spotless {
    kotlin {
        ktlint("1.5.0")
        endWithNewline()
        targetExclude("**/build/**")
    }
}

Pre-commit hook (.git/hooks/pre-commit via pre-commit or simple shell):

./gradlew spotlessCheck detekt

CI runs same.


§2. Dependency Management

Versions catalog

gradle/libs.versions.toml:

[versions]
kotlin = "2.3.21"
spring-boot = "3.5.2"
hibernate = "6.6.49"
liquibase = "5.0.2"
postgres-jdbc = "42.7.4"
keycloak = "26.6.1"
mapstruct = "1.6.3"
testcontainers = "1.20.4"
kotest = "5.9.1"
mockk = "1.13.13"
micrometer = "1.13.7"
opentelemetry = "1.41.0"
ktlint = "1.5.0"
detekt = "1.23.7"
springdoc = "2.7.0"

[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "spring-boot" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "spring-boot" }
hibernate-core = { module = "org.hibernate.orm:hibernate-core", version.ref = "hibernate" }
liquibase-core = { module = "org.liquibase:liquibase-core", version.ref = "liquibase" }
mapstruct-core = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" }
mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstruct" }
testcontainers-postgres = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }
testcontainers-redpanda = { module = "org.testcontainers:redpanda", version.ref = "testcontainers" }
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
kotlin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
ksp = { id = "com.google.devtools.ksp", version = "2.3.21-1.0.27" }
spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.7" }

Update process

Cadence What Tool
Daily Security patches Dependabot
Weekly Patch updates Dependabot grouped PR
Monthly Minor updates Manual review
Quarterly Major updates ADR for significant ones

Verification checklist (before merging dep bump)

  • Read upstream changelog
  • CI fully green
  • No new HIGH/CRITICAL CVEs introduced
  • Local smoke test: docker compose up && demo.sh
  • If transitive deps changed: review SBOM diff
  • If breaking: write ADR, plan migration

Forbidden dependencies

Library Reason
commons-collections (versions before 3.2.2 / 4.1) known RCE chains
log4j-core < 2.17.1 Log4Shell
spring-boot < 3.0 EOL
Any AGPL-licensed library conflicts with BSL strategy

Pre-commit gitleaks scan + Trivy in CI.


§3. Git Flow (trunk-based with release branches)

Branches

Branch Purpose Lifetime
main Always green, deployable forever
feature/<issue>-<slug> Single feature < 3 days
fix/<issue>-<slug> Single bug fix < 1 day
release/<minor> Release branch (e.g., release/0.1) until next minor
hotfix/<issue> Critical fix off a release tag until merged

Branch protection (main)

- Require PR before merging
- Require 1+ approval
- Dismiss stale reviews on push
- Require status checks: ci/build, ci/test, ci/lint, ci/security, dependency-review
- Require branches up-to-date
- Require signed commits
- Require linear history (squash or rebase only)
- Restrict force push (admins only with explicit reason)

Conventional Commits

<type>(<scope>): <subject>

<body>

<footer>

Types:

  • feat: - new feature
  • fix: - bug fix
  • docs: - documentation
  • style: - formatting, no code change
  • refactor: - code change without feature/bug change
  • test: - adding/fixing tests
  • chore: - tooling/build/CI
  • breaking!: - breaking change (also in body)

Examples:

feat(ledger): add time-travel balance endpoint

Adds GET /v1/accounts/{id}/balance?asOf=<ISO-8601>.
Replays entries up to timestamp.

Refs #42
fix(payments): preserve idempotency-key on retry to bank

Bank adapter was generating new keys per attempt.
Now derives `payment-bank-${idempotencyKey}-${attempt}`.

Closes #87

PR workflow

  1. Create branch from main
  2. Push commits (Conventional)
  3. Open PR with template (linked issue, test plan)
  4. Wait for CI
  5. Request review
  6. Address feedback in new commits
  7. Merge: squash + Conventional commit message (preserves clean history)
  8. Branch auto-deleted

Squash vs rebase vs merge

We use squash merge in main:

  • Each PR = 1 commit in main
  • Linear history
  • Easy revert
  • CHANGELOG generated from squash messages

Rebases allowed within a PR branch (pre-merge).

Merge commits forbidden in main.


§4. Release Flow

SemVer 2.0

MAJOR.MINOR.PATCH:

  • MAJOR - breaking changes (rare, well-prepared)
  • MINOR - new features, backward-compatible
  • PATCH - bug fixes, security patches

Release cadence

Release Cadence
Patch as needed (within hours for security)
Minor ~6 weeks
Major 12-18 months

Release branch model

main
 │
 ├─── release/0.1 ─── tag v0.1.0 ──── tag v0.1.1 (patch, cherry-picked) ──── tag v0.1.2
 │
 ├─── release/0.2 ─── tag v0.2.0 ────...
 │
 └─── ...

When approaching a minor release:

  1. Cut release/0.x from main
  2. Tag v0.x.0 from release/0.x
  3. Patch fixes go to main first, then cherry-picked to release/0.x for v0.x.y patches
  4. Each tag automatically:
    • Pushes Docker images to GHCR (multi-arch arm64+amd64)
    • Packages and publishes Helm chart
    • Generates GitHub Release notes (from Conventional Commits via release-please)
    • Updates CHANGELOG
    • Signs artifacts with cosign

LTS policy

  • Current minor + previous minor receive security fixes for 6 months after the next minor releases
  • Older minors archived

Release-please integration

.github/release-please-config.json:

{
  "release-type": "simple",
  "bump-minor-pre-major": true,
  "bump-patch-for-minor-pre-major": true,
  "draft": false,
  "prerelease": false
}

Generates release PR automatically when Conventional commits accumulate.

Release artifacts checklist

  • Tag pushed
  • Docker images on GHCR (signed, SBOM attached)
  • Helm chart packaged & on gh-pages chart repo
  • Maven artifacts (decision-engine library) on Maven Central
  • Release notes (auto-generated from Conventional Commits)
  • CHANGELOG.md updated
  • Migration notes (if breaking)
  • Blog post (for v0.x.0+ minor releases)
  • Newsletter sent

§5. ADR Process

When to write an ADR

  • New significant architectural choice
  • Major dependency adoption (Hibernate, Keycloak, broker)
  • Security-relevant decision
  • Performance trade-off worth documenting
  • Anything we'd want to revisit / explain later

Don't write ADRs for:

  • Style preferences (covered by Code-Rules)
  • Specific bug fixes (commit message is enough)
  • Features (covered by Epic + Use Case)

Format (Michael Nygard template)

# ADR-XXXX: Short title

**Status**: Proposed | Accepted | Deprecated | Superseded
**Date**: YYYY-MM-DD
**Decider**: Maintainer

## Context
What's the situation? What constraint led to this decision?

## Decision
What we're doing.

## Consequences
### Positive
### Negative
### Neutral

## Alternatives considered
### Alternative A
### Alternative B

## Validation
How we verify this is the right call.

## Related
Links to other ADRs, blog posts, code.

Workflow

  1. Propose: branch adr/XXXX-title, write initial ADR, open PR with status Proposed
  2. Discuss: PR review, refine context, alternatives, decision
  3. Accept: merge PR, ADR is now Accepted
  4. Immutable: never edit accepted ADRs (except formatting / typos)
  5. Supersede: write new ADR Status: Accepted, supersedes ADR-NNNN; old becomes Status: Superseded

Numbering

Sequential, four digits, never reused. Check ADR-Index for next number.

Where stored

In Wiki pages: ADR-NNNN-<short-slug>. Linked from ADR-Index.

In repo: archived copies in docs/adr/ (autosynced from Wiki via CI, optional).


Related

Clone this wiki locally