Technical Decision-Making
Technical leads choose among imperfect options under uncertainty. Ranked decisions make trade-offs explicit, document wrong choices, and set review triggers so Rust stack choices (Axum vs Actix, sync vs async DB) do not become permanent religion.
How to Use This List
- Walk decisions in order when planning architecture or upgrades.
- Record the chosen rank and why in an ADR or ticket.
- Set a review trigger (scale, team size, SLO miss) for reversible calls.
- Use spikes to de-risk "2nd" options before committing to "Best."
- Revisit when trigger fires - supersede ADR, do not silently drift.
Decision 1: HTTP framework for a new Rust API
Scenario: Greenfield B2B API, 6 engineers, SLO p95 200ms, Postgres, need OpenAPI.
| Rank | Choice | Approach |
|---|---|---|
| Best | Axum 0.8 | Tower middleware, tracing, serde extractors, ecosystem alignment |
| 2nd | Actix-web | Mature, fast; different middleware model |
| 3rd | Hyper raw | Maximum control; more boilerplate |
Wrong choice: Hyper-only for 40+ endpoints without shared internal framework - inconsistent auth and validation.
Why best is best: Team SLO and Tower ecosystem favor Axum; internal auth crate already targets Axum.
Decision 2: Async Tokio vs threaded server
Scenario: CRUD API, mostly Postgres I/O, occasional CPU on PDF generation.
| Rank | Choice | Approach |
|---|---|---|
| Best | Tokio + spawn_blocking for CPU | Async I/O; isolate CPU on blocking pool |
| 2nd | Multi-thread std server (rare) | Only if no async deps and tiny scale |
| 3rd | Blocking Axum handlers everywhere | Blocks worker threads on I/O |
Wrong choice: std::fs and sync HTTP client inside async handlers - runtime stalls.
Why best is best: Workload is I/O bound; team standard is Tokio 1.x with known patterns.
Decision 3: Monolith vs split services
Scenario: 8 engineers, one product, shared Postgres, deploy weekly.
| Rank | Choice | Approach |
|---|---|---|
| Best | Modular monolith (workspace crates) | Clear crate boundaries; single deploy; extract later |
| 2nd | Two binaries (API + worker) | When queue scale and failure domain differ |
| 3rd | Microservices per domain | Premature ops tax |
Wrong choice: Five microservices before second team exists - CFR and lead time suffer.
Why best is best: Team size and transaction boundaries fit one deployable workspace.
Decision 4: sqlx async vs sync pool
Scenario: Axum service considering async sqlx pool.
| Rank | Choice | Approach |
|---|---|---|
| Best | Async sqlx with Tokio | Natural fit for Axum; pool metrics via tracing |
| 2nd | Sync pool + spawn_blocking | If team avoids async DB patterns |
| 3rd | Raw tokio-postgres only | Loses compile-time query checks |
Wrong choice: Async pool with blocking ORM-style calls in handlers - pool exhaustion masked as latency.
Why best is best: Squad uses sqlx 0.8 offline mode in CI; async path is documented.
Decision 5: Error type strategy
Scenario: Application binary plus shared internal library crates.
| Rank | Choice | Approach |
|---|---|---|
| Best | thiserror in libs, anyhow at binary edge | Typed public errors; context at main |
| 2nd | thiserror everywhere | Verbose in CLI tools with many exit codes |
| 3rd | anyhow everywhere | Fast prototype; weak library contracts |
Wrong choice: unwrap in library public API - panics become consumer incidents.
Why best is best: Matches Rust API guidelines; maps cleanly to Axum IntoResponse.
Decision 6: Serialization for public API
Scenario: JSON REST API with evolving fields.
| Rank | Choice | Approach |
|---|---|---|
| Best | serde with explicit #[serde(deny_unknown_fields)] on admin APIs | Safe parsing; versioned DTOs |
| 2nd | Protobuf / gRPC (tonic) | High-throughput internal services |
| 3rd | Manual JSON Value | Loses schema documentation |
Wrong choice: Untagged serde_json::Value for all bodies - validation scattered, bugs in prod.
Why best is best: OpenAPI generation and client SDKs align with serde models.
FAQs
How long should decision meetings run?
45 minutes max; output is accept, revise RFC, or time-boxed spike.
Who breaks ties?
Named decision maker (tech lead or EM) after options are written; record dissent.
Revisit decisions when?
When review trigger fires: 10x traffic, team +50%, repeated SLO miss.
Spike duration?
2-3 days with binary success criteria and throwaway branch.
Document reversible decisions?
Light ADR or ticket comment still worth it - prevents re-debate monthly.
Polars vs DataFusion?
Polars 0.46+ for in-memory/analytical batch; separate decision for streaming SQL.
CLI clap derive vs builder?
Derive for stable CLIs; builder for dynamic subcommands - ADR if public plugin API.
When involve security?
Auth, crypto, unsafe, FFI, and dependency spikes touching network boundary.
Decision without consensus?
Disagree and commit; dissent in ADR consequences with revisit trigger.
Template location?
docs/adr/ or RFC folder linked from team README.
Related
- Architecture Decision Records (ADRs) - record outcomes
- Running Design Reviews - large decisions
- Handling Technical Disagreements - conflict process
- Spikes, PoCs & Adopting New Crates - de-risk options
- Leadership Best Practices - summary list
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.