The Art of Technical Decision-Making Under Uncertainty
A framework for sound technical decisions when requirements are incomplete and stakes are high: reversibility analysis, decision records and ambiguity.

Every Decision Is Made with Incomplete Information
The defining characteristic of technical decision-making is that you never have enough information. Requirements change. Performance characteristics emerge only under load. Integration behavior surfaces only in production. Waiting for perfect information means shipping nothing.
The skill is not eliminating uncertainty—it is making good decisions despite it. This requires a systematic approach to classifying decisions, assessing reversibility, and documenting the reasoning so future teams understand not just what was decided but why.
Classifying Decisions by Reversibility
Jeff Bezos famously described Type 1 decisions (irreversible, one-way doors) and Type 2 decisions (reversible, two-way doors). Most technical decisions are Type 2, but teams treat them all as Type 1—over-analyzing, committee-reviewing, and delaying until the decision is made for them by deadline.
interface TechnicalDecision {
id: string;
title: string;
context: string;
options: DecisionOption[];
type: "type-1" | "type-2";
reversibilityCost: "trivial" | "moderate" | "significant" | "prohibitive";
timeToReverse: string;
decisionDate: string;
deciders: string[];
outcome?: string;
}
interface DecisionOption {
name: string;
pros: string[];
cons: string[];
risks: Risk[];
estimatedEffort: string;
reversibilityPlan: string;
}
interface Risk {
description: string;
likelihood: "low" | "medium" | "high";
impact: "low" | "medium" | "high";
mitigation: string;
}
// ❌ Treating every decision the same way
function handleDecision(decision: TechnicalDecision): string {
return "Schedule a meeting with all stakeholders";
}
// ✅ Matching decision process to decision type
function handleDecisionByType(decision: TechnicalDecision): string {
if (decision.type === "type-2") {
return decision.reversibilityCost === "trivial"
? "Decide now, one person, document briefly"
: "Quick discussion with 2-3 people, decide within a day";
}
// Type 1: Irreversible decisions deserve careful analysis
return "Write a detailed decision record, gather input from affected teams, set a decision deadline";
}The Decision Record as a Thinking Tool
Architecture Decision Records (ADRs) are not bureaucracy—they are a forcing function for clear thinking. Writing down the context, options, and rationale reveals gaps in your reasoning that verbal discussion hides.
interface ADR {
number: number;
title: string;
status: "proposed" | "accepted" | "deprecated" | "superseded";
date: string;
context: string;
decision: string;
consequences: {
positive: string[];
negative: string[];
neutral: string[];
};
alternatives: Array<{
option: string;
rejected: string; // Why this was not chosen
}>;
supersededBy?: number;
}
function createADR(input: {
title: string;
context: string;
options: Array<{
name: string;
analysis: string;
}>;
chosenOption: string;
rationale: string;
}): ADR {
const chosen = input.options.find(
(o) => o.name === input.chosenOption
);
const rejected = input.options.filter(
(o) => o.name !== input.chosenOption
);
return {
number: getNextADRNumber(),
title: input.title,
status: "proposed",
date: new Date().toISOString().split("T")[0],
context: input.context,
decision: `We will use ${input.chosenOption}. ${input.rationale}`,
consequences: {
positive: [], // Fill during review
negative: [],
neutral: [],
},
alternatives: rejected.map((opt) => ({
option: opt.name,
rejected: opt.analysis,
})),
};
}Decision-Making Under Time Pressure
When the deadline is tomorrow and the architecture question is unresolved, you need a framework that produces a defensible decision quickly. The RAPID framework assigns clear roles: Recommend, Agree, Perform, Input, Decide.
interface RAPIDDecision {
recommender: string; // Proposes the solution
agreers: string[]; // Must agree (blockers)
performers: string[]; // Will implement
inputProviders: string[]; // Consulted for expertise
decider: string; // Makes the final call
}
interface TimeboxedDecision {
decision: TechnicalDecision;
rapid: RAPIDDecision;
deadline: Date;
fallback: string; // What happens if no decision by deadline
}
function evaluateUnderPressure(
decision: TechnicalDecision,
timeAvailable: number // hours
): {
approach: string;
analysisDepth: "shallow" | "moderate" | "deep";
requiredParticipants: number;
} {
if (timeAvailable < 2) {
return {
approach: "Choose the most reversible option. " +
"Document the decision and revisit in one week.",
analysisDepth: "shallow",
requiredParticipants: 1,
};
}
if (timeAvailable < 8) {
return {
approach: "List top 2-3 options. Score on reversibility " +
"and alignment with existing architecture. " +
"Quick sync with one other engineer.",
analysisDepth: "moderate",
requiredParticipants: 2,
};
}
return {
approach: "Full ADR process. Evaluate all options against " +
"stated criteria. Seek input from affected teams.",
analysisDepth: "deep",
requiredParticipants: 3,
};
}Spike-Driven Decision-Making
When analysis alone cannot resolve a decision, build the smallest possible prototype to generate real data. A two-day spike produces more useful information than two weeks of debate.
interface Spike {
question: string;
hypothesis: string;
timebox: string;
successCriteria: string[];
deliverables: string[];
decision: string; // What will be decided based on results
}
// ❌ Debating database choice for weeks with no data
// "Let's discuss whether PostgreSQL or DynamoDB is better
// for our access patterns"
// ✅ Running a spike to generate concrete data
const databaseSpike: Spike = {
question: "Can DynamoDB handle our query patterns " +
"at the required latency?",
hypothesis: "DynamoDB single-table design can serve " +
"our 5 primary access patterns under 10ms p99",
timebox: "2 days",
successCriteria: [
"All 5 access patterns implemented",
"Load test at 2x projected traffic",
"p99 latency measured for each pattern",
"Cost estimate at projected scale",
],
deliverables: [
"Benchmark results for each access pattern",
"DynamoDB table design document",
"Cost projection spreadsheet",
"Go/no-go recommendation",
],
decision: "If all patterns meet latency targets, " +
"proceed with DynamoDB. Otherwise, use PostgreSQL.",
};
function evaluateSpikeResults(
spike: Spike,
results: Record<string, boolean>
): {
recommendation: string;
confidence: "high" | "medium" | "low";
openQuestions: string[];
} {
const metCriteria = Object.values(results).filter(Boolean).length;
const totalCriteria = Object.keys(results).length;
const successRate = metCriteria / totalCriteria;
if (successRate === 1) {
return {
recommendation: "Proceed with the proposed approach",
confidence: "high",
openQuestions: [],
};
}
if (successRate >= 0.7) {
const failed = Object.entries(results)
.filter(([, passed]) => !passed)
.map(([criteria]) => criteria);
return {
recommendation: "Proceed with mitigations for failed criteria",
confidence: "medium",
openQuestions: failed,
};
}
return {
recommendation: "Pursue alternative approach",
confidence: "high",
openQuestions: [],
};
}Learning from Past Decisions
The most valuable part of a decision record is the retrospective update. Six months after a decision, you know whether it was right. Writing that down builds institutional knowledge about which decision patterns work and which do not.
interface DecisionRetrospective {
adrNumber: number;
originalDecision: string;
retrospectiveDate: string;
outcome: "validated" | "partially-validated" | "invalidated";
surprises: string[];
lessonsLearned: string[];
wouldChangeApproach: boolean;
whatWouldChange: string;
}
function generateRetrospectivePrompt(
adr: ADR
): string[] {
return [
`Did the decision achieve its intended goals?`,
`What consequences occurred that we didn't predict?`,
`Were the rejected alternatives actually better in hindsight?`,
`What information would have changed the decision?`,
`Would we make the same decision again with current knowledge?`,
`What should future teams know about this decision's real-world impact?`,
];
}Key Takeaways
The quality of technical decisions depends not on having complete information but on having a systematic process for navigating uncertainty. Classify every decision by reversibility first—most are Type 2 and deserve hours of analysis, not weeks. Write decision records not as documentation but as a thinking tool that reveals gaps in your reasoning.
When time is short, choose the most reversible option and document it. When analysis stalls, run a timeboxed spike to generate real data. The two-day prototype that produces benchmarks beats the two-week debate that produces opinions.
The decision itself is only half the value. The retrospective—written six months later with real production data—is where institutional knowledge accumulates. Teams that systematically review past decisions make better future decisions, not because they remember every detail, but because they develop calibrated intuition about which patterns of reasoning lead to good outcomes.


