244 lines
9.9 KiB
Markdown
244 lines
9.9 KiB
Markdown
---
|
|
title: "Architecting Low-Latency Decision Engines: One-Way vs. Two-Way Door Metrics in Software Architecture"
|
|
description: "How to design low-latency software decision engines using Feature Flags, State Machines, and Bezos' reversible decision framework in TypeScript."
|
|
tags: ["architecture", "typescript", "systemdesign", "devops"]
|
|
canonical_url: "https://entscheidomat.com/ratgeber/entscheidung-treffen-wenn-zwei-optionen-gleich-gut-sind"
|
|
target_keywords: ["entscheidungsfinder", "entscheidungsgenerator", "entweder oder generator", "feature flag architecture", "state machine typescript"]
|
|
---
|
|
|
|
# Architecting Low-Latency Decision Engines: One-Way vs. Two-Way Door Metrics in Software Architecture
|
|
|
|
In high-throughput distributed systems, decision-making happens millions of times per second. Whether an application is routing API traffic between microservices, executing A/B testing variations, evaluating user entitlements, or powering a lightweight decision tool like an [Entscheidungsgenerator](https://entscheidomat.com/entweder-oder), how decision engines are architected directly impacts **latency, system resilience, and deployment velocity**.
|
|
|
|
Amazon founder Jeff Bezos famously divided all decisions into two distinct categories in his 1997 Shareholder Letter:
|
|
* **Type 1 Decisions (One-Way Doors):** Irreversible, high-consequence architectural bets (e.g. primary database migration, multi-region cloud strategy).
|
|
* **Type 2 Decisions (Two-Way Doors):** Reversible, low-risk operational choices (e.g. UI micro-interactions, algorithm tweaks, feature toggles).
|
|
|
|
In this article, we will translate this decision framework into concrete software architecture patterns. We will build a production-grade, zero-dependency **Type 2 Decision Engine with Feature Flag Rollouts and Deterministic Finite State Machines (FSM)** in TypeScript.
|
|
|
|
---
|
|
|
|
## 1. Architectural Taxonomy: Type 1 vs. Type 2 Decisions in Code
|
|
|
|
Software architecture mistakes occur when engineering organizations treat Type 2 decisions with Type 1 rigor (paralyzing release cycles) or treat Type 1 decisions as Type 2 shortcuts (causing catastrophic outage vulnerabilities).
|
|
|
|
```text
|
|
┌────────────────────────┐
|
|
│ Software Decision Flow │
|
|
└───────────┬────────────┘
|
|
│
|
|
Is the change easily reversible?
|
|
│
|
|
┌──────────────┴──────────────┐
|
|
▼ ▼
|
|
YES (Type 2 / Two-Way) NO (Type 1 / One-Way)
|
|
┌───────────────────────────┐ ┌──────────────────────────┐
|
|
│ • Feature Toggles │ │ • DB Schema Migrations │
|
|
│ • Dynamic API Routing │ │ • Monolith to Serverless │
|
|
│ • Progressive Rollouts │ │ • Protocol Format Shifts │
|
|
└───────────────────────────┘ └──────────────────────────┘
|
|
```
|
|
|
|
| Decision Metric | Type 1 (One-Way Door) | Type 2 (Two-Way Door) |
|
|
| :--- | :--- | :--- |
|
|
| **Reversibility Cost** | Extremely High ($O(\text{Weeks/Months})$) | Near Zero ($O(\text{Milliseconds})$) |
|
|
| **Evaluation Speed Target** | Weeks (RFCs, Architecture Reviews) | $< 1 \text{ms}$ (Runtime Evaluator) |
|
|
| **Verification Strategy** | Formal proofs, load testing | Feature flags, canary rollouts, A/B metrics |
|
|
| **Code Implementation** | Immutable schema contracts | Dynamic state machine / Config evaluation |
|
|
|
|
---
|
|
|
|
## 2. Designing a Sub-Millisecond Type 2 Decision Engine
|
|
|
|
Let's build a sub-millisecond, zero-dependency **Type 2 Feature Flag & Routing Engine** in TypeScript. This engine allows teams to make instant, reversible runtime decisions without re-deploying code.
|
|
|
|
```typescript
|
|
export interface UserContext {
|
|
id: string;
|
|
email: string;
|
|
country: string;
|
|
isBetaTester: boolean;
|
|
}
|
|
|
|
export interface FeatureFlagRule {
|
|
id: string;
|
|
enabled: boolean;
|
|
rolloutPercentage: number; // 0 to 100
|
|
allowedCountries?: string[];
|
|
requiresBeta?: boolean;
|
|
}
|
|
|
|
export class DecisionEngine {
|
|
private rules: Map<string, FeatureFlagRule> = new Map();
|
|
|
|
public registerRule(rule: FeatureFlagRule): void {
|
|
this.rules.set(rule.id, rule);
|
|
}
|
|
|
|
/**
|
|
* Deterministic hash calculation (MurmurHash3 variant) to ensure
|
|
* a user consistently receives the same feature bucket.
|
|
*/
|
|
private hashUser(userId: string, flagId: string): number {
|
|
const key = `${userId}:${flagId}`;
|
|
let hash = 0x811c9dc5; // FNV-1a offset basis
|
|
|
|
for (let i = 0; i < key.length; i++) {
|
|
hash ^= key.charCodeAt(i);
|
|
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
}
|
|
|
|
return (hash >>> 0) % 100; // Returns consistent integer 0..99
|
|
}
|
|
|
|
/**
|
|
* Evaluates a decision rule in under 0.1 milliseconds.
|
|
*/
|
|
public evaluate(flagId: string, context: UserContext): boolean {
|
|
const rule = this.rules.get(flagId);
|
|
|
|
if (!rule || !rule.enabled) return false;
|
|
|
|
// Rule 1: Beta Check
|
|
if (rule.requiresBeta && !context.isBetaTester) return false;
|
|
|
|
// Rule 2: Geo-location Filter
|
|
if (rule.allowedCountries && !rule.allowedCountries.includes(context.country)) {
|
|
return false;
|
|
}
|
|
|
|
// Rule 3: Deterministic Percentage Rollout
|
|
const userBucket = this.hashUser(context.id, flagId);
|
|
return userBucket < rule.rolloutPercentage;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Performance & Determinism Test
|
|
```typescript
|
|
const engine = new DecisionEngine();
|
|
engine.registerRule({
|
|
id: "new_checkout_flow",
|
|
enabled: true,
|
|
rolloutPercentage: 25, // 25% rollout
|
|
allowedCountries: ["DE", "AT", "CH"]
|
|
});
|
|
|
|
const user: UserContext = {
|
|
id: "usr_94821",
|
|
email: "dev@example.com",
|
|
country: "DE",
|
|
isBetaTester: false
|
|
};
|
|
|
|
const startTime = performance.now();
|
|
const isEnabled = engine.evaluate("new_checkout_flow", user);
|
|
const duration = performance.now() - startTime;
|
|
|
|
console.log(`Decision Evaluation Result: ${isEnabled} (Executed in ${duration.toFixed(4)} ms)`);
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Reversible Decision Workflows via Finite State Machines (FSM)
|
|
|
|
For complex multi-step application states (such as checkout flows, loan applications, or interactive decision tools like [Entscheidomat Entweder-Oder](https://entscheidomat.com/entweder-oder)), representing state transitions as a **Finite State Machine** ensures strict type safety and zero invalid transitions.
|
|
|
|
```typescript
|
|
export type DecisionState = "IDLE" | "EVALUATING" | "RESOLVED" | "CANCELLED";
|
|
export type DecisionEvent = "SUBMIT" | "APPROVE" | "REJECT" | "RESET";
|
|
|
|
export class ReversibleDecisionFSM {
|
|
private currentState: DecisionState = "IDLE";
|
|
private history: DecisionState[] = [];
|
|
|
|
private readonly transitions: Record<DecisionState, Partial<Record<DecisionEvent, DecisionState>>> = {
|
|
IDLE: { SUBMIT: "EVALUATING" },
|
|
EVALUATING: { APPROVE: "RESOLVED", REJECT: "CANCELLED", RESET: "IDLE" },
|
|
RESOLVED: { RESET: "IDLE" },
|
|
CANCELLED: { RESET: "IDLE" }
|
|
};
|
|
|
|
public transition(event: DecisionEvent): DecisionState {
|
|
const allowedNextState = this.transitions[this.currentState][event];
|
|
|
|
if (!allowedNextState) {
|
|
throw new Error(`Invalid FSM transition: Cannot trigger '${event}' from state '${this.currentState}'`);
|
|
}
|
|
|
|
this.history.push(this.currentState);
|
|
this.currentState = allowedNextState;
|
|
return this.currentState;
|
|
}
|
|
|
|
/**
|
|
* Reverts to previous state (Two-Way Door behavior)
|
|
*/
|
|
public rollback(): DecisionState {
|
|
const previous = this.history.pop();
|
|
if (!previous) {
|
|
throw new Error("No previous state to rollback to");
|
|
}
|
|
|
|
this.currentState = previous;
|
|
return this.currentState;
|
|
}
|
|
|
|
public getState(): DecisionState {
|
|
return this.currentState;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Benchmark & Architectural Checklist
|
|
|
|
When designing modern web applications and decision utilities:
|
|
|
|
| Principle | Technical Implementation |
|
|
| :--- | :--- |
|
|
| **Sub-Millisecond Evaluation** | Use in-memory FNV-1a hashing instead of remote DB network calls on every request. |
|
|
| **Zero Deployment Rollbacks** | Wrap all Type 2 changes in feature flag evaluation blocks. |
|
|
| **Reversibility (Two-Way)** | Implement FSM history stacks to allow single-click state rollbacks. |
|
|
| **Stateless Scalability** | Derive user buckets deterministically using `hash(userId + flagId) % 100`. |
|
|
|
|
---
|
|
|
|
## Conclusion & Practical Takeaway
|
|
|
|
1. Software teams must explicitly tag changes as **Type 1 (One-Way)** or **Type 2 (Two-Way)** in RFCs and PR reviews.
|
|
2. Type 2 decisions should never require code deployments; evaluate them using in-memory feature flags and state machines.
|
|
3. For interactive consumer tools, use clean TypeScript state engines to keep execution fast, predictable, and reversible.
|
|
|
|
Test an interactive decision engine live on [Entscheidomat Entweder-Oder](https://entscheidomat.com/entweder-oder).
|
|
|
|
---
|
|
|
|
## FAQ (Schema Structured Data)
|
|
|
|
```json
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "FAQPage",
|
|
"mainEntity": [
|
|
{
|
|
"@type": "Question",
|
|
"name": "What is Jeff Bezos' One-Way vs. Two-Way Door decision framework?",
|
|
"acceptedAnswer": {
|
|
"@type": "Answer",
|
|
"text": "One-Way Door (Type 1) decisions are irreversible and high-stakes, requiring slow evaluation. Two-Way Door (Type 2) decisions are easily reversible and should be executed rapidly using feature flags and runtime evaluators."
|
|
}
|
|
},
|
|
{
|
|
"@type": "Question",
|
|
"name": "How fast should a feature flag decision engine evaluate?",
|
|
"acceptedAnswer": {
|
|
"@type": "Answer",
|
|
"text": "A well-architected in-memory decision engine using deterministic hashing should evaluate in under 0.1 milliseconds per request without hitting external network databases."
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|