Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Programming Languages

Welcome to the Languages section of the Dita Engineering Guidelines. Here you will find specific standards, best practices, and tooling requirements for the programming languages we use.

Supported Languages

  • Rust - Guidelines for writing safe, concurrent, and practical systems in Rust.

Goals of this section:

  • Ensure consistency across codebases.
  • Promote language-specific idiomatic practices.
  • Define standard tooling and configuration (linters, formatters).

Rust Engineering Guidelines

This section details the standards and best practices for Rust development at Dita. Our goal is to leverage Rust's safety guarantees while maintaining readable and maintainable code.

Core Principles

  1. Safety First: Avoid unsafe code unless absolutely necessary and strictly isolated/documented.
  2. Idiomatic Rust: Follow standard conventions. If "fighting the borrow checker", reconsider the design.
  3. Documentation: All public APIs must be documented.

Tooling

  • Formatter: We use rustfmt with the standard configuration.
  • Linter: We use clippy. CI pipelines should fail on clippy warnings.

Topics

Rust project structure — rust-project-structure.md

Below is a recommended repository layout for a Rust service that follows Clean Architecture principles and a functional-programming-friendly style. After the tree I describe the purpose and accepted contents of each entry so maintainers and contributors know what belongs where.

/                                 # repo root
├── .github/                      # CI/CD pipelines
│   └── workflows/
│       ├── common.yml
│       ├── feature.yml
│       ├── dev.yml
│       ├── master.yml
│       └── release.yml
│
├── docker/                       # Docker build context (multi-stage, test images, compose helpers)
│   ├── Dockerfile
│   ├── docker-compose.yml
│   └── README.md
│
├── proto/                        # Shared gRPC contracts (.proto files)
│   ├── student.proto
│   ├── instructor.proto
│   └── common.proto
│
├── scripts/                      # Dev & Ops helper scripts (migrate, seed, local-run)
│   ├── migrate.sh
│   ├── seed.sh
│   ├── local-run.sh
│   └── README.md
│
├── migrations/                   # SQL migrations (for sqlx / refinery)
│   ├── 0001_init.sql
│   ├── 0002_add_instructor_table.sql
│   └── README.md
│
├── src/
│   ├── main.rs                   # Bootstrap entry: load config, init telemetry, start runtime
│   │
│   ├── bin/                      # Optional binaries (e.g., worker, migrate, consumer)
│   │   ├── migrate.rs
│   │   └── consumer.rs
│   │
│   ├── core/                     # ❖ Domain & Use Cases — pure, testable, framework-free
│   │   ├── domain/               # Entities, ValueObjects, domain errors, domain events
│   │   │   ├── student.rs
│   │   │   ├── instructor.rs
│   │   │   ├── error.rs
│   │   │   └── mod.rs
│   │   │
│   │   ├── ports/                # Abstract traits (repositories, message brokers, cache)
│   │   │   ├── student_repository.rs
│   │   │   ├── instructor_repository.rs
│   │   │   ├── message_broker.rs
│   │   │   ├── error.rs
│   │   │   └── mod.rs
│   │   │
│   │   └── usecases/             # Application layer orchestration
│   │       ├── enroll_student/
│   │       │   ├── command.rs
│   │       │   ├── query.rs
│   │       │   └── tests.rs
│   │       │
│   │       ├── assign_instructor/
│   │       │   ├── command.rs
│   │       │   ├── query.rs
│   │       │   └── tests.rs
│   │       │
│   │       └── mod.rs
│   │
│   ├── adapters/                 # ❖ Adapters — implementing external boundaries (infra)
│   │   ├── http/                 # Axum controllers + route definitions
│   │   │   ├── routes.rs
│   │   │   └── handlers/
│   │   │       ├── student_handler.rs
│   │   │       ├── instructor_handler.rs
│   │   │       └── mod.rs
│   │   │
│   │   ├── grpc/                 # tonic server + client implementations
│   │   │   ├── student_grpc.rs
│   │   │   ├── instructor_grpc.rs
│   │   │   └── mod.rs
│   │   │
│   │   ├── persistence/          # SQLx repositories (Postgres)
│   │   │   ├── student_repository.rs
│   │   │   ├── instructor_repository.rs
│   │   │   └── mod.rs
│   │   │
│   │   ├── kafka/                # rdkafka producers & consumers
│   │   │   ├── producer.rs
│   │   │   ├── consumer.rs
│   │   │   └── mod.rs
│   │   │
│   │   ├── cache/                # Redis adapters (deadpool-redis)
│   │   │   ├── redis_cache.rs
│   │   │   └── mod.rs
│   │   │
│   │   ├── auth/                 # JWT / OIDC middleware
│   │   │   ├── jwt.rs
│   │   │   └── mod.rs
│   │   │
│   │   └── external/             # third-party HTTP/gRPC integrations
│   │       ├── payment_service.rs
│   │       ├── logging_service.rs
│   │       └── mod.rs
│   │
│   ├── infrastructure/           # ❖ Composition root: wiring + builders + runtime setup
│   │   ├── bootstrap.rs          # app startup orchestration
│   │   ├── config-loader.rs      # config loader using `config` crate (YAML + env)
│   │   ├── db.rs                 # Postgres connection builder
│   │   ├── cache.rs              # Redis pool builder
│   │   ├── kafka.rs              # Kafka setup
│   │   ├── cli.rs                # Command-line interface definition and parsing
│   │   ├── server/               # Axum + Tonic server builders
│   │   │   ├── http_server.rs
│   │   │   ├── grpc_server.rs
│   │   │   └── mod.rs
│   │   ├── telemetry/            # tracing, otel, prometheus exporters
│   │   │   ├── tracing.rs
│   │   │   ├── metrics.rs
│   │   │   └── mod.rs
│   │   └── mod.rs
│   │
│   ├── shared/                   # Common utils shared across layers (pure helpers)
│   │   ├── error.rs              # unified AppError type
│   │   ├── json.rs               # serde/json helpers
│   │   ├── time.rs               # chrono/date helpers
│   │   ├── config.rs             # Application configuration model
│   │   ├── state.rs
│   │   └── result.rs             # functional Result combinators / error adapters
│   │
│   ├── tests/                    # Integration & E2E tests
│   │   ├── integration/
│   │   │   ├── db_integration_test.rs
│   │   │   ├── kafka_integration_test.rs
│   │   │   └── mod.rs
│   │   └── e2e/
│   │       ├── user_flow_test.rs
│   │       └── mod.rs
│   │
│   └── lib.rs                    # Optional if the service exposes internal lib functions
│
├── config.yml
├── config-dev.yml
├── config-stage.yml
├── config-prod.yml
├── Cargo.toml
└── README.md

Rust Service Configuration

All DiTA Rust backend services must follow this configuration loading pattern.

Configuration loading must be implemented under the infrastructure layer:

src/
└── infrastructure/
    └── config_loader.rs

The implementation must use the config crate for configuration sources and serde for typed deserialization.

Default Configuration

The default configuration must be embedded into the binary using include_str!:

#![allow(unused)]
fn main() {
static DEFAULT_YAML: &str = include_str!("../../config.yaml");
}

It must be registered as the first configuration source:

#![allow(unused)]
fn main() {
Config::builder()
    .add_source(File::from_str(DEFAULT_YAML, FileFormat::Yaml))
}

The default configuration therefore does not depend on the presence of the file at runtime.

Configuration Files

Configuration files are loaded after the embedded defaults.

The environment is determined by APP_ENV:

#![allow(unused)]
fn main() {
let env = env::var("APP_ENV").unwrap_or_else(|_| "dev".into());
}

If no explicit configuration path is provided, the loader must consider these paths in order:

./config.yml
./config-{APP_ENV}.yml
/etc/{application-name}/config.yml

The application name must be obtained from Cargo:

#![allow(unused)]
fn main() {
let app_name = env!("CARGO_PKG_NAME");
}

The default paths are optional:

#![allow(unused)]
fn main() {
File::from(path.to_path_buf()).required(false)
}

An explicitly provided configuration path must be required:

#![allow(unused)]
fn main() {
File::from(path.to_path_buf()).required(true)
}

Environment Variables

Environment variables must be loaded after all configuration files:

#![allow(unused)]
fn main() {
.add_source(
    Environment::with_prefix("APP")
        .separator("__")
)
}

The APP prefix is common to all services.

Nested configuration fields must use __ as the separator. For example:

APP_DATABASE__HOST=localhost
APP_DATABASE__PORT=5432

corresponds to:

database:
  host: localhost
  port: 5432

Because environment variables are added last, they have the highest precedence.

The resulting precedence is:

Embedded defaults
        ↓
Configuration files
        ↓
Environment variables

Typed Configuration

The final configuration must be deserialized into a service-specific type:

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
pub struct Config {
    pub server: ServerConfig,
    pub database: DatabaseConfig,
}
}

The loader must use a generic deserialization interface:

#![allow(unused)]
fn main() {
pub fn load<C>(conf_path: &Option<String>) -> C
where
    C: DeserializeOwned + Debug,
}

Application and domain code must use the resulting typed configuration rather than reading environment variables directly.

Error Handling

Configuration loading and deserialization must fail fast.

The loader must log failures using tracing before terminating:

#![allow(unused)]
fn main() {
.inspect_err(|error| {
    error!("Config Load -> FAILED: error=({})", error)
})
.unwrap()
}

and:

#![allow(unused)]
fn main() {
.inspect_err(|error| {
    error!("Config Deserialize -> FAILED: error=({})", error)
})
.unwrap();
}

A service must not start with an invalid or incomplete configuration.

Logging

Successful configuration loading should be logged:

#![allow(unused)]
fn main() {
info!(
    "Config Load -> SUCCESS: [path, required]=({:?}), config=({:?})",
    conf_paths, config
);
}

Configuration values must not be logged if their Debug representation can expose secrets such as passwords, tokens, API keys, or private keys.

Implementation

The configuration loader should follow this implementation pattern:

#![allow(unused)]
fn main() {
use config::{Config, Environment, File, FileFormat};
use serde::de::DeserializeOwned;
use std::env;
use std::fmt::Debug;
use std::path::PathBuf;
use tracing::{error, info};

static DEFAULT_YAML: &str = include_str!("../../config.yaml");

pub fn load<C>(conf_path: &Option<String>) -> C
where
    C: DeserializeOwned + Debug,
{
    let conf_paths = get_config_paths(conf_path);

    let config = conf_paths
        .iter()
        .map(|(path, required)| File::from(path.to_path_buf()).required(required.clone()))
        .fold(
            Config::builder().add_source(File::from_str(DEFAULT_YAML, FileFormat::Yaml)),
            |builder, file| builder.add_source(file),
        )
        .add_source(Environment::with_prefix("APP").separator("__"))
        .build()
        .inspect_err(|error| error!("Config Load -> FAILED: error=({})", error))
        .unwrap()
        .try_deserialize()
        .inspect_err(|error| error!("Config Deserialize -> FAILED: error=({})", error))
        .unwrap();

    info!(
        "Config Load -> SUCCESS: [path, required]=({:?}), config=({:?})",
        conf_paths, config
    );

    config
}

fn get_config_paths(conf_path: &Option<String>) -> Vec<(PathBuf, bool)> {
    let env = env::var("APP_ENV").unwrap_or_else(|_| "dev".into());

    let cwd = env::current_dir()
        .inspect_err(|error| {
            error!("Get Current Working Directory -> FAILED. error=({})", error)
        })
        .unwrap();

    let app_name = env!("CARGO_PKG_NAME");

    match conf_path {
        None => vec![
            (cwd.join("config.yml"), false),
            (cwd.join(format!("config-{}.yml", env)), false),
            (cwd.join(format!("/etc/{}/config.yml", app_name)), false),
        ],
        Some(path) => vec![(cwd.join(path), true)],
    }
}
}

This implementation pattern must be used consistently across Rust backend services.

Rust Service Bootstrap

All DiTA Rust backend services must follow a standardized bootstrap pattern.

The application entry point must remain minimal. Service initialization, command-line parsing, configuration loading, application state construction, router initialization, and server startup must be orchestrated by the infrastructure bootstrap layer.

Architecture

The bootstrap flow must follow this structure:

src/
├── main.rs
├── infrastructure/
│   ├── bootstrap.rs
│   └── cli.rs
└── shared/
    ├── config.rs
    └── state.rs

The responsibilities of these modules are:

ModuleResponsibility
main.rsApplication entry point
infrastructure/bootstrap.rsApplication bootstrap orchestration
infrastructure/cli.rsCommand-line interface definition and parsing
shared/config.rsApplication configuration model
shared/state.rsShared application state

Application Entry Point

main.rs must contain only the minimal application entry point and delegate startup to the bootstrap layer.

async fn main() -> AppResult<()> {
    bootstrap::start().await
}

Business logic, configuration parsing, router construction, or infrastructure initialization must not be implemented directly in main.rs.

Command-Line Interface

Application bootstrap configuration must be initiated through a command-line interface implemented with clap.

The CLI definition must reside under the infrastructure layer:

src/
└── infrastructure/
    └── cli.rs

The CLI must use clap::Parser:

#![allow(unused)]
fn main() {
use clap::Parser;

#[derive(Parser)]
#[command(name = "<app name>")]
#[command(about = "<App Description>", long_about = None)]
pub struct Cli {
    #[arg(short, long, global = true)]
    pub config: Option<String>,

    // ...
}
}

Cli::parse() must be the entry point for command-line argument parsing during bootstrap.

Services may expose additional command-line arguments as required by their responsibilities, but CLI options must remain limited to application startup and operational concerns.

Configuration Loading

The parsed CLI arguments must be used to determine the configuration source.

Configuration loading must remain separate from the CLI definition and must use the project's standardized configuration-loading mechanism.

The configuration model must be defined independently:

#![allow(unused)]
fn main() {
use serde::Deserialize;
use std::net::SocketAddr;

#[derive(Clone, Debug, Deserialize)]
pub struct Config {
    pub listen_addr: SocketAddr,

    // ...
}
}

The bootstrap layer is responsible for connecting the CLI configuration path to the configuration loader:

#![allow(unused)]
fn main() {
let cli = Cli::parse();
let conf_path = cli.config;
let config: Config = config_loader::load(&conf_path);
}

The configuration model must not depend on clap, and the CLI model must not contain application runtime state.

Bootstrap Orchestration

All service startup operations must be orchestrated by infrastructure/bootstrap.rs.

The bootstrap function must perform initialization in a defined sequence:

  1. Parse command-line arguments.
  2. Load application configuration.
  3. Initialize application state.
  4. Initialize application components and infrastructure dependencies.
  5. Construct the router and middleware.
  6. Bind the server listener.
  7. Start the HTTP server.

A typical implementation is:

#![allow(unused)]
fn main() {
pub async fn start() -> AppResult<()> {
    telemetry::init();
    info!("Starting <service name>...");

    let cli = Cli::parse();
    let conf_path = cli.config;
    let config: Config = config_loader::load(&conf_path);

    let state = AppState {
        config: config.clone(),
        // ...
    };

    let app = router()
        .with_state(Arc::new(state))
        .layer(TraceLayer::new_for_http());

    let listener = tokio::net::TcpListener::bind(&config.listen_addr).await?;

    info!(
        "Listening -> SUCCESS: listen_addr=({})",
        config.listen_addr
    );

    axum::serve(listener, app).await?;

    info!(
        "Axum Serve -> SUCCESS: listen_addr=({})",
        config.listen_addr
    );

    Ok(())
}
}

The bootstrap function is responsible for orchestration only. It must not become a container for business logic or domain-specific application behavior.

Shared Application State

Application-wide runtime state must be represented by AppState and defined separately from the bootstrap implementation:

src/
└── shared/
    └── state.rs

Example:

#![allow(unused)]
fn main() {
pub struct AppState {
    pub config: Config,

    // ...
}
}

The bootstrap layer constructs the initial AppState and provides it to the application components that require shared state.

AppState must contain runtime dependencies and shared application data, not command-line parsing concerns.

Dependency Boundaries

The following boundaries must be maintained:

main.rs
   │
   ▼
bootstrap.rs
   │
   ├──► cli.rs
   ├──► config_loader
   ├──► Config
   ├──► AppState
   └──► application infrastructure

The CLI, configuration model, and application state have distinct responsibilities:

  • cli.rs defines how startup parameters are provided.
  • config.rs defines the application's runtime configuration.
  • state.rs defines shared runtime state.
  • bootstrap.rs connects these components and starts the service.

HTTP Server Startup

HTTP server construction and startup must be performed by the bootstrap layer.

For Axum-based services:

#![allow(unused)]
fn main() {
let listener = tokio::net::TcpListener::bind(&config.listen_addr).await?;
axum::serve(listener, app).await?;
}

The listener address must originate from application configuration rather than being hard-coded in the bootstrap implementation.

Bootstrap Errors

Bootstrap failures must be propagated through the service's standard application result type:

#![allow(unused)]
fn main() {
pub async fn start() -> AppResult<()> {
    // ...
}
}

Errors encountered while loading configuration, constructing infrastructure, binding the listener, or starting the server must not be silently ignored.

Design Rules

All DiTA Rust backend services must follow these rules:

  • main.rs must remain a thin entry point.
  • clap must be used for command-line parsing.
  • CLI definitions must reside in infrastructure/cli.rs.
  • Bootstrap orchestration must reside in infrastructure/bootstrap.rs.
  • Runtime configuration must be represented by a dedicated Config type.
  • Shared runtime dependencies must be represented by AppState.
  • Configuration loading must remain separate from CLI parsing.
  • Business logic must not be implemented in the bootstrap layer.
  • Server addresses and other runtime parameters must come from configuration rather than hard-coded values.
  • Bootstrap failures must be propagated through the service's standard error handling mechanism.

This pattern provides a consistent startup contract across all DiTA Rust backend services while keeping the application entry point, CLI, configuration, state management, and infrastructure initialization clearly separated.

Development Guidelines

Architecture Decision Records (ADRs)

Architecture Decision Records (ADRs) are short text documents that capture an important architectural decision made along with its context and consequences. We use ADRs to keep track of the history of our decisions and the reasoning behind them.

File Location

All ADRs must be placed in the doc project.

Naming Convention

ADR files should be named using a sequential number and a short descriptive title, separated by a hyphen. The format is NNNN-title-of-the-decision.md.

Example: 0001-record-architecture-decisions.md

Statuses

An ADR can have one of the following statuses:

  • Proposed: The decision is being discussed and has not yet been agreed upon.
  • Accepted: The decision has been agreed upon and should be implemented.
  • Rejected: The decision was proposed and discussed but was not accepted. The record is kept for historical context.
  • Deprecated: The decision was accepted in the past but is no longer applicable or recommended.
  • Superseded: The decision has been replaced by a newer decision. The new ADR should reference the superseded one.

Template

Use the following template for new ADRs:

# NNNN. Title of the Decision

Date: YYYY-MM-DD

## Status

[Proposed | Accepted | Rejected | Deprecated | Superseded]

## Context

What is the issue that we're seeing that is motivating this decision or change?

## Decision

What is the change that we're proposing and/or doing?

## Consequences

What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.

How to Submit

  1. Create a new branch for your ADR.
  2. Add the new ADR file in doc project.
  3. Ensure the file name uses the next available number.
  4. Submit a Pull Request for review.

Git Workflow

This section outlines our Git workflow, integrating branching, committing, and issue management.

  1. Branching: Create a new branch for each task. See Branching Strategy.
  2. Committing: Write clear, conventional commit messages. See Commit Guidelines.
  3. Issues: Link your work to specific issues. See Issue Management.
  4. ADRs: Reference architectural decisions if applicable. See ADR Linking.

Workflow Steps

  1. Pick an issue (or create one).
  2. Create a branch from dev (for new features) or master (for incident fixes).
  3. Implement changes.
  4. Commit changes following the Commit Guidelines.
  5. Push branch and open a Pull Request.
  6. Ensure CI checks (including commitlint) pass.

Branching Strategy

We use a strategy allowing for parallel development using short-lived branches. The repository has two persistent branches: main and dev.

Core Branches

  • master: The production-ready state. Contains stable code.
  • dev: The main development branch. All new features are merged here first.

Branch Naming & Strategy

Feature Branches

Used for adding new functionality.

  • Source Branch: dev
  • Naming Convention: Must include the issue number.
  • Format: feat/issue-<number>/<short-description>
  • Example: feat/issue-42/add-login-page

Bug Fix Branches

Used for fixing critical bugs or incidents in production.

  • Source Branch: master (preferred for hotfixes/incidents)
  • Naming Convention: Must include the incident number.
  • Format: fix/incident-<number>/<short-description>
  • Example: fix/incident-808/resolve-memory-leak

Other Branches

For documentation, refactoring, or chores.

  • Source Branch: dev (usually)
  • Format: type/<scope>/<short-description> or type/issue-<number>/<short-description>
  • Example: docs/readme/update-setup

Branches should be deleted after merging.

Commit Guidelines

High-quality commit messages are essential for maintaining a healthy codebase. They help new developers understand context, simplify debugging with git blame, and allow for automated changelog generation.

Message Structure

A commit message consists of a Header, Body, and Footer.

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

<body (optional, but recommended)>

<footer (optional)>

1. The Header

The header is mandatory and must be 50 characters or less.

Type

Must be one of the following:

TypeDescriptionSemVer Impication
featA new featureMINOR
fixA bug fixPATCH
docsDocumentation changesPATCH
styleFormatting, missing semi-colons, white-space (no code change)PATCH
refactorCode change that neither fixes a bug nor adds a featurePATCH
perfA code change that improves performancePATCH
testAdding missing tests or correcting existing testsPATCH
buildChanges that affect the build system or external dependenciesPATCH
ciChanges to our CI configuration files and scriptsPATCH
choreOther changes that don't modify src or test filesPATCH
revertReverts a previous commitPATCH

Scope

The scope is optional but recommended. It specifies the "place" of the commit change.

  • Examples: auth, api, ui, database, deps.
  • Format: feat(auth): ...

Subject

The subject contains a succinct description of the change.

  • Imperative mood: "Add" not "Added", "Fix" not "Fixed".
  • No period: Do not end with ..
  • LowerCase: First letter is usually lowercase (Conventional Commits doesn't enforce this, but consistency is key. Note: "Seven Rules" says capitalized, Conventional Commits examples often show lowercase. We will follow lowercase to match the commitlint default config unless configured otherwise.). -> Self-correction: The previous file said "Capitalize". "Seven Rules" says Capitalize. Conventional Commits is agnostic. Let's stick to Capitalize as it looks more professional in Git logs, matching the previous rule.

2. The Body

The body is optional but strongly recommended for non-trivial changes.

  • Wrap at 72 characters: This ensures readability in all environments.
  • Motivation: Explain why you are making this change.
  • Contrast: Compare the new behavior with the previous behavior.
  • Explanation: Explain what and why, not how (the code explains the how).

The footer is used for meta-information.

Breaking Changes

All breaking changes have to be mentioned in the footer with the description of the change, justification and migration notes.

  • Format: Start with BREAKING CHANGE: <description>
  • Alternative: Add ! after the type/scope in the header (e.g., feat(api)!: ...).

References

  • Issues: Closes #123, Fixes #42.
  • ADRs: ADR: 004.

Collaboration

  • Co-authored-by: Co-authored-by: Name <name@example.com>

Examples

Feature with Scope

feat(auth): Add Google OAuth login support

Add support for Google OAuth 2.0 to allow users to sign up
and log in using their Google accounts. This simplifies the
onboarding process.

Closes #101

Bug Fix with Breaking Change

fix(api): Handle null values in user response

Previously, the API crashed when the user had no address.
Now, it returns a null address field instead.

BREAKING CHANGE: The `address` field in the user object
can now be null. Clients must update their code to handle
this case.

Documentation

docs(readme): Update installation instructions

Revert

revert: let us never speak of this again

This reverts commit 6b2a412.

Issue Management

Properly linking Git activity to issues ensures a transparent history and automated workflow states.

Linking in Commits

Reference issues in your commit footer.

Closing Issues

To automatically close an issue when the commit is merged:

Closes #123

Or for multiple issues:

Closes #123, #245

Referencing Issues

To simply reference an issue without closing it (e.g., "See also"):

Refs #123

Linking in Pull Requests

In the description of your Pull Request, you can also use keywords to link issues.

  • "Closes #123"
  • "Fixes #123"
  • "Resolves #123"

This creates a link in GitHub and automatically closes the issue when the PR is merged into the default branch.

ADR Linking

Architecture Decision Records (ADRs) explain the "why" behind significant changes. It is crucial to link code changes to the decisions that authorized them.

Linking ADRs in Commits

If a commit implements a specific ADR, reference it in the footer of the commit message, similar to issues.

Format:

ADR: <ADR-Number>

Example:

feat(database): migrate to postgres

We are migrating to Postgres to support better transaction handling as decided.

Closes #45
ADR: 0012

Linking ADRs in Issues/PRs

When opening an Issue or PR that relates to an architectural decision, include a link to the ADR file in the description.

Example:

Implements ADR-0012: Use Postgres