-
Notifications
You must be signed in to change notification settings - Fork 0
Dev Guides
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 |
| 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 |
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 = falsecomplexity:
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: errorplugins {
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 detektCI runs same.
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" }| 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 |
- 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
| 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.
| 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 |
- 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)
<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
- Create branch from
main - Push commits (Conventional)
- Open PR with template (linked issue, test plan)
- Wait for CI
- Request review
- Address feedback in new commits
- Merge: squash + Conventional commit message (preserves clean history)
- Branch auto-deleted
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.
MAJOR.MINOR.PATCH:
-
MAJOR- breaking changes (rare, well-prepared) -
MINOR- new features, backward-compatible -
PATCH- bug fixes, security patches
| Release | Cadence |
|---|---|
| Patch | as needed (within hours for security) |
| Minor | ~6 weeks |
| Major | 12-18 months |
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:
- Cut
release/0.xfrom main - Tag
v0.x.0fromrelease/0.x - Patch fixes go to main first, then cherry-picked to
release/0.xforv0.x.ypatches - 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
- Current minor + previous minor receive security fixes for 6 months after the next minor releases
- Older minors archived
.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.
- Tag pushed
- Docker images on GHCR (signed, SBOM attached)
- Helm chart packaged & on
gh-pageschart 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
- 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)
# 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.-
Propose: branch
adr/XXXX-title, write initial ADR, open PR with statusProposed - Discuss: PR review, refine context, alternatives, decision
-
Accept: merge PR, ADR is now
Accepted - Immutable: never edit accepted ADRs (except formatting / typos)
-
Supersede: write new ADR
Status: Accepted, supersedes ADR-NNNN; old becomesStatus: Superseded
Sequential, four digits, never reused. Check ADR-Index for next number.
In Wiki pages: ADR-NNNN-<short-slug>. Linked from ADR-Index.
In repo: archived copies in docs/adr/ (autosynced from Wiki via CI, optional).
- Code-Rules - Kotlin/Spring conventions
- Testing-Strategy - test categorization
- ADR-Index - all ADRs
- Architecture-Overview - what these decisions support
- CONTRIBUTING.md - entry point for new contributors
- Overview
- Services
- Data Model
- Domain Model
- Event Flow
- Security
- Observability
- Resilience
- SLA / SLI / SLO