Definition
- Falsification Conditions – specific, measurable evidence that would invalidate an architectural choice and trigger a redesign.
- Redis Justification – assumes a hot set read far more than written, high enough read volume that the DB cannot cope, and latency gain worth the operational cost.
- Async Messaging Justification – assumes the caller does not need an immediate authoritative result and can tolerate eventual consistency.
- Microservice Boundaries Justification – assumes independent scaling, deployment, and ownership along genuine domain seams with low cross-boundary coupling.
- Epistemic Humility – the ability to describe conditions under which your own design becomes wrong, proving it was evidence-based.
- Architecture Decision Record (ADR) – pairs each assumption with the condition that would invalidate it, reviewed against live metrics on a cadence.
When National Bank of India decides to open a new express counter, a good branch manager does not just say "express counters are good." They say "I am opening this because more than one in three customers only need a quick balance check; if that drops below one in ten, this counter is wasting a clerk and I will close it." That second sentence -- the condition under which the decision becomes wrong -- is what makes the manager trustworthy.
Understanding the Concepts
Why is falsification the mark of principal-level thinking?
Junior engineers defend decisions. Senior engineers falsify them. Each choice is a bet on assumptions, and the professional move is to make those assumptions explicit with measurable triggers.
package com.bank.architecture;
public class FalsificationMindset {
public static void main(String[] args) {
System.out.println("Junior: 'We should use Redis because everyone does.'");
System.out.println("Senior: 'We should use Redis because our read hit rate is 95%");
System.out.println(" and DB read utilization is at 80%. If hit rate drops below 60%");
System.out.println(" or DB utilization drops below 30%, Redis becomes unnecessary.'");
System.out.println();
System.out.println("The second answer can be TESTED and REVISITED.");
System.out.println("The first answer is fashion.");
}
}
Junior: 'We should use Redis because everyone does.'
Senior: 'We should use Redis because our read hit rate is 95%
and DB read utilization is at 80%. If hit rate drops below 60%
or DB utilization drops below 30%, Redis becomes unnecessary.'
The second answer can be TESTED and REVISITED.
The first answer is fashion.
When would Redis be unnecessary?
Redis is justified only when there is a hot set of data read far more than written, the read volume exceeds what the database can serve, and the latency improvement justifies the operational cost.
package com.bank.architecture;
public class RedisJustification {
public static void main(String[] args) {
double hitRate = 0.95;
double dbReadUtilization = 0.80;
System.out.println("Cache hit rate: " + (hitRate * 100) + "%");
System.out.println("DB read utilization: " + (dbReadUtilization * 100) + "%");
System.out.println("Redis justified: YES (high hit rate, DB under pressure)");
System.out.println();
System.out.println("Evidence Redis is UNNECESSARY:");
System.out.println(" - Hit rate < 60% (most reads miss, cache is just an extra hop)");
System.out.println(" - DB utilization < 30% (DB's own buffer cache copes)");
System.out.println(" - Data set < 100MB (in-process Caffeine suffices, no network hop)");
}
}
Cache hit rate: 95.0%
DB read utilization: 80.0%
Redis justified: YES (high hit rate, DB under pressure)
Evidence Redis is UNNECESSARY:
- Hit rate < 60% (most reads miss, cache is just an extra hop)
- DB utilization < 30% (DB's own buffer cache copes)
- Data set < 100MB (in-process Caffeine suffices, no network hop)
When would synchronous communication be preferable to async?
Async messaging assumes the caller does not need an immediate answer and can tolerate eventual consistency. When those assumptions fail, synchronous is better.
package com.bank.architecture;
public class AsyncVsSync {
public static void main(String[] args) {
System.out.println("Async justified when:");
System.out.println(" - Caller does not need immediate result");
System.out.println(" - Eventual consistency is acceptable");
System.out.println(" - Need to decouple producer/consumer");
System.out.println();
System.out.println("Evidence SYNC is preferable:");
System.out.println(" - POS authorization: must approve/decline before customer leaves");
System.out.println(" - Balance check: caller needs the answer NOW to proceed");
System.out.println(" - Simple request-response: broker overhead buys nothing");
}
}
Async justified when:
- Caller does not need immediate result
- Eventual consistency is acceptable
- Need to decouple producer/consumer
Evidence SYNC is preferable:
- POS authorization: must approve/decline before customer leaves
- Balance check: caller needs the answer NOW to proceed
- Simple request-response: broker overhead buys nothing
When would microservice boundaries be wrong?
Microservice boundaries assume independent scaling, deployment, and ownership along genuine domain seams.
package com.bank.architecture;
public class MicroserviceBoundaries {
public static void main(String[] args) {
System.out.println("Microservices justified when:");
System.out.println(" - Services change for different reasons at different rates");
System.out.println(" - Distinct scaling profiles");
System.out.println(" - Separate teams own them with clear contracts");
System.out.println(" - Cross-boundary coupling < 20%");
System.out.println();
System.out.println("Evidence boundaries are WRONG:");
System.out.println(" - Most features require coordinated deploys across 3+ services");
System.out.println(" - One small team owns all services, drowning in distributed tax");
System.out.println(" - Services share a database, coupling through it anyway");
System.out.println(" → Modular monolith gives same separation without the overhead");
}
}
Microservices justified when: - Services change for different reasons at different rates - Distinct scaling profiles - Separate teams own them with clear contracts - Cross-boundary coupling < 20% Evidence boundaries are WRONG: - Most features require coordinated deploys across 3+ services - One small team owns all services, drowning in distributed tax - Services share a database, coupling through it anyway → Modular monolith gives same separation without the overhead
Why does stating falsification conditions build trust?
Three reasons: it proves the decision was reasoned, gives measurable triggers for revisiting, and builds trust because the designer volunteers weaknesses.
package com.bank.architecture;
public class TrustBuilding {
public static void main(String[] args) {
System.out.println("Reason 1: Proves decision was evidence-based, not fashion");
System.out.println(" Only a reasoned decision has identifiable assumptions.");
System.out.println();
System.out.println("Reason 2: Gives concrete, measurable triggers to revisit");
System.out.println(" 'If hit rate < 60%, drop Redis' → objective, not political.");
System.out.println();
System.out.println("Reason 3: Builds trust with reviewers and leadership");
System.out.println(" An engineer who volunteers weaknesses is reasoning honestly.");
}
}
Reason 1: Proves decision was evidence-based, not fashion Only a reasoned decision has identifiable assumptions. Reason 2: Gives concrete, measurable triggers to revisit 'If hit rate < 60%, drop Redis' → objective, not political. Reason 3: Builds trust with reviewers and leadership An engineer who volunteers weaknesses is reasoning honestly.
How do you formalize this with Architecture Decision Records?
Pair each assumption with the condition that would invalidate it, and review against live metrics on a cadence.
package com.bank.architecture;
public class ArchitectureDecisionRecord {
public static void main(String[] args) {
System.out.println("ADR-007: Use Redis for account balance cache");
System.out.println(" Assumption: hit rate > 80%, DB read util > 60%");
System.out.println(" Falsification: hit rate < 60% OR DB util < 30%");
System.out.println(" Review cadence: quarterly");
System.out.println(" Current metrics: hit rate 95%, DB util 80%");
System.out.println(" Status: VALID");
System.out.println();
System.out.println("ADR-012: Microservice boundary for statement service");
System.out.println(" Assumption: separate team, independent deploy, low coupling");
System.out.println(" Falsification: >50% features require cross-service deploy");
System.out.println(" Current metrics: 15% cross-service deploys");
System.out.println(" Status: VALID");
}
}
ADR-007: Use Redis for account balance cache
Assumption: hit rate > 80%, DB read util > 60%
Falsification: hit rate < 60% OR DB util < 30%
Review cadence: quarterly
Current metrics: hit rate 95%, DB util 80%
Status: VALID
ADR-012: Microservice boundary for statement service
Assumption: separate team, independent deploy, low coupling
Falsification: >50% features require cross-service deploy
Current metrics: 15% cross-service deploys
Status: VALID
What are common mistakes?
Defending a decision by authority or fashion rather than evidence and falsifiable assumptions.
package com.bank.architecture;
// WRONG: "We use Redis because Netflix uses Redis."
// No assumptions stated, no triggers to revisit, no way to objectively evaluate
// CORRECT: "We use Redis because hit rate is 95% and DB read util is 80%.
// If hit rate drops below 60%, we will remove it."
// Evidence-based, falsifiable, reviewable
What are common interview follow-up questions?
- How do you balance the cost of change against the cost of keeping a decision that may be wrong?
- What is an Architecture Decision Record and how do you use it?
- How do you decide when an assumption has been violated enough to warrant a redesign?
- What is the difference between a reversible and an irreversible architectural decision?
Visual Overview
Architecture Falsification Framework ───────────────────────────────────── For each decision: ┌────────────────────────────────────────┐ │ Decision: Use Redis for balance cache │ │ │ │ Assumptions: │ │ • Hit rate > 80% │ │ • DB read utilization > 60% │ │ • Latency gain justifies cost │ │ │ │ Falsification triggers: │ │ • Hit rate < 60% → remove Redis │ │ • DB util < 30% → remove Redis │ │ • Data < 100MB → use Caffeine │ │ │ │ Review: quarterly against live metrics │ └────────────────────────────────────────┘ Trustworthy architecture = decision + its own expiry conditions
Key Takeaway
The mark of principal-level thinking is not confidence in a design but the ability to state precisely what would prove it wrong. Every architectural choice rests on assumptions, and the professional move is to make those assumptions explicit with measurable triggers.
If you recommend Redis, the falsification is a low cache hit rate or a database with spare read capacity. If you chose async messaging, the falsification is a use case needing an immediate authoritative answer. If you proposed microservices, the falsification is that most features require coordinated deploys and one team drowns in distributed overhead.
An architecture is much easier to trust when the designer states its own falsification conditions, because it proves the choice was reasoned from evidence, gives concrete triggers to revisit, and builds trust with reviewers. Good architecture is a time-bound fit to current constraints, and the designer's job includes marking the expiry conditions so the system evolves deliberately.
Remember one sentence: a trustworthy architecture comes with its own falsification conditions, because an engineer who can describe when their design becomes wrong proves it was reasoned, not fashionable.
---