home /
writings /
thoughts /
courses /
research /
projects /
Back to writings

3 Mental Models and Building the Right Intuition

Feb 1, 2026 | 10 min read

tl;dr: Three essential mental models for engineers: systems thinking, trade-offs and constraints, and abstractions. Build pattern recognition and develop engineering intuition.

Great engineers don’t just write code—they develop intuition. They recognize patterns, anticipate problems, and make decisions that seem obvious in hindsight but require deep understanding.

This intuition comes from mental models: frameworks for thinking about complex systems. Here are three that have shaped how I approach engineering problems.

Mental Model 1: Systems Thinking

Systems thinking is understanding that components don’t exist in isolation. They interact, creating emergent behavior that can’t be predicted by examining parts individually.

Systems Thinking: Component Interactions

graph TB
    Core[Core System] --> DB[Database]
    Core --> Cache[Cache]
    Core --> API[API Gateway]
    Core --> MQ[Message Queue]
    DB <--> Cache
    API <--> MQ
    Cache -->|Cache Warming| MQ
    MQ -->|Async Processing| API
    
    Note1[Emergent Behavior:<br/>System performance depends on<br/>all component interactions]
    Note2[Feedback Loops:<br/>Changes in one component<br/>affect others cascading]

The Core Idea

A system is more than the sum of its parts. When you change one component, you affect others—sometimes in ways you didn’t anticipate.

Example: Adding a Cache

You add Redis to speed up database queries. Simple, right?

But now:

  • Your database load decreases (good)
  • Memory usage increases (trade-off)
  • Cache invalidation becomes critical (new problem)
  • Stale data can cause bugs (new failure mode)
  • Cache warming affects cold starts (new consideration)

The cache doesn’t just “make things faster.” It changes how your entire system behaves.

How to Apply It

1. Map Dependencies

Before making changes, draw the dependency graph:

API Gateway → Auth Service → Database
         ↓
    Cache Layer
         ↓
    Message Queue → Background Workers

Ask: “If I change X, what else is affected?“

2. Think in Feedback Loops

Systems have feedback loops. A slow database causes timeouts, which cause retries, which cause more load, which makes the database slower.

Recognize these cycles early:

// Bad: Creates feedback loop
async function handleRequest() {
  if (cacheMiss) {
    const data = await slowDatabaseQuery();
    await cache.set(data); // If this is slow, more cache misses
    return data;
  }
}

// Better: Break the loop
async function handleRequest() {
  if (cacheMiss) {
    // Use circuit breaker to prevent cascading failures
    if (circuitBreaker.isOpen()) {
      return staleCacheData; // Graceful degradation
    }
    const data = await slowDatabaseQuery();
    await cache.set(data);
    return data;
  }
}

3. Consider Emergent Behavior

Emergent behavior is what happens when components interact in unexpected ways.

Real Example: A microservices architecture where each service is fast individually, but the system is slow because of network latency between services. The slowness “emerges” from the interactions.

How to handle it:

  • Monitor the system, not just components
  • Use distributed tracing to see the full picture
  • Design for the system, optimize components second

Building Intuition

Practice systems thinking by:

  1. When debugging: Don’t just look at the error. Ask: “What else changed? What else is affected?”
  2. When designing: Draw the system diagram first. Show all interactions.
  3. When optimizing: Measure the system, not just the component.

The best engineers see the forest and the trees.

Mental Model 2: Trade-offs and Constraints

Every engineering decision is a trade-off. There’s no perfect solution—only solutions optimized for specific constraints.

Trade-offs and Constraints

graph TB
    Fast[Fast]
    Cheap[Cheap]
    Good[Good]
    
    Fast -.->|Trade-off| Cheap
    Cheap -.->|Trade-off| Good
    Good -.->|Trade-off| Fast
    
    Note[Pick Two:<br/>You can't have everything]

The Core Idea

You can’t have everything. Fast, cheap, and good—pick two. (Sometimes you only get one.)

The Trade-off Triangle:

  • Speed vs. Quality: Ship fast or build it right?
  • Cost vs. Performance: Use more servers or optimize code?
  • Simplicity vs. Flexibility: Hard-code or make it configurable?

Common Trade-offs in Practice

1. Caching

Benefits: Fast reads, reduced database load
Costs: Stale data, memory usage, invalidation complexity

Decision framework:

  • How often does data change? (If rarely → cache is worth it)
  • How critical is freshness? (If critical → shorter TTL or write-through)
  • What’s the memory budget? (If limited → selective caching)
// Trade-off: Memory vs. Performance
const cache = new LRUCache({
  max: 1000, // Trade-off: More items = more memory, better hit rate
  ttl: 300000 // Trade-off: Longer TTL = more stale data, fewer DB calls
});

2. Microservices vs. Monolith

Microservices: Independent scaling, technology diversity
Costs: Network overhead, distributed system complexity, eventual consistency

When to choose:

  • Monolith: Small team, simple domain, need strong consistency
  • Microservices: Large team, complex domain, need independent scaling

3. SQL vs. NoSQL

SQL: ACID guarantees, strong consistency, mature tooling
Costs: Scaling challenges, schema rigidity

NoSQL: Horizontal scaling, schema flexibility
Costs: Weaker consistency, less mature tooling

The trade-off: Consistency vs. Scale

How to Apply It

1. Make Trade-offs Explicit

Document your decisions:

## Decision: Use Redis for Session Storage

**Chosen:** Redis (in-memory cache)
**Trade-offs:**
- Fast reads/writes
- Horizontal scaling
- Data lost on restart (mitigated with persistence)
- Memory cost

**Alternatives considered:**
- Database: Too slow for session lookups
- Memcached: No persistence, less features

2. Optimize for Your Context

The “best” solution depends on your constraints:

  • Startup: Optimize for speed (ship fast, refactor later)
  • Enterprise: Optimize for reliability (thorough testing, gradual rollout)
  • High-traffic: Optimize for performance (caching, CDN, optimization)

3. Revisit as Constraints Change

Trade-offs aren’t permanent. As your system grows, constraints change:

// Early stage: Optimize for development speed
const config = {
  database: 'sqlite', // Simple, no setup
  cache: 'none' // Premature optimization
};

// Growth stage: Optimize for performance
const config = {
  database: 'postgres', // Better performance, features
  cache: 'redis' // Now worth the complexity
};

Building Intuition

Practice recognizing trade-offs by:

  1. When choosing a technology: List 3 pros and 3 cons. What are you giving up?
  2. When optimizing: What are you optimizing for? What are you sacrificing?
  3. When reviewing code: Ask: “What trade-offs did the author make? Are they appropriate?”

The best engineers make informed trade-offs, not perfect solutions.

Mental Model 3: Abstractions and Leaky Abstractions

Abstractions hide complexity, making us more productive. But they always leak—the underlying complexity shows through.

Abstraction Layers

graph TD
    App[Application Layer<br/>Business Logic]
    Framework[Framework Layer<br/>React, Express, Django]
    Library[Library Layer<br/>Axios, Lodash]
    Runtime[Runtime Layer<br/>Node.js, JVM]
    OS[Operating System<br/>Linux, Windows]
    Hardware[Hardware<br/>CPU, Memory, Disk]
    
    App --> Framework
    Framework --> Library
    Library --> Runtime
    Runtime --> OS
    OS --> Hardware
    
    Note[All Abstractions Leak:<br/>Understand one level below<br/>to debug effectively]

The Core Idea

Every abstraction has a cost. The more it hides, the more it can surprise you when things go wrong.

The Abstraction Stack:

Application (Your Code)
    ↓
Framework (React, Express)
    ↓
Libraries (Axios, Lodash)
    ↓
Runtime (Node.js, JVM)
    ↓
Operating System
    ↓
Hardware

Each layer hides complexity from the one above. But when something breaks, you need to understand the layer below.

Leaky Abstractions in Practice

1. ORMs (Object-Relational Mappers)

ORMs abstract away SQL, but SQL leaks through:

// Looks simple
const users = await User.findAll({
  where: { active: true },
  limit: 10
});

// But what if this generates N+1 queries?
users.forEach(user => {
  console.log(user.profile.name); // Query per user!
});

// You need to understand SQL to fix it
const users = await User.findAll({
  where: { active: true },
  include: [{ model: Profile }], // Eager loading
  limit: 10
});

The leak: You need SQL knowledge to write efficient ORM code.

2. Cloud Platforms

AWS abstracts away servers, but infrastructure leaks through:

  • Lambda cold starts (serverless abstraction)
  • VPC networking (network abstraction)
  • IAM permissions (security abstraction)

You can’t ignore infrastructure just because it’s “abstracted.”

3. JavaScript Frameworks

React abstracts away DOM manipulation, but the DOM leaks through:

// React abstraction
function Component() {
  useEffect(() => {
    // But you still need to understand:
    // - Event loop
    // - Browser APIs
    // - Memory leaks
    // - Re-render cycles
  }, []);
}

How to Apply It

1. Understand One Level Down

You don’t need to be an expert in every layer, but understand the one below:

  • Writing React? Understand JavaScript and the browser
  • Using ORMs? Understand SQL
  • Deploying to AWS? Understand Linux and networking

2. Know When to Drop Down

Sometimes you need to bypass the abstraction:

// Abstraction is slow
const result = await orm.query('SELECT * FROM users');

// Drop down to raw SQL
const result = await db.raw('SELECT id, name FROM users WHERE active = ?', [true]);

3. Choose the Right Level

Not everything needs maximum abstraction:

  • High-level: Business logic, user-facing features
  • Low-level: Performance-critical code, system integration
// High abstraction: Good for business logic
const order = await Order.create({ userId, items });

// Low abstraction: Good for performance
const order = await db.transaction(async (tx) => {
  // Manual transaction control for complex logic
});

Building Intuition

Practice working with abstractions by:

  1. When debugging: Ask: “What’s happening one level below?”
  2. When learning: Learn the abstraction, then learn what it abstracts
  3. When choosing: Prefer simpler abstractions. Complexity compounds.

The best engineers know when to use abstractions and when to drop down.

Building Pattern Recognition

These mental models help you recognize patterns:

Systems Thinking → “This looks like a feedback loop” Trade-offs → “We’re optimizing for X, so Y will suffer” Abstractions → “This abstraction is leaking, I need to understand the layer below”

How to Develop Intuition

1. Study Systems, Not Just Code

Read architecture docs, system design articles, post-mortems. See how others solved similar problems.

2. Reflect on Decisions

After making a decision, ask:

  • What trade-offs did I make?
  • What systems are affected?
  • What abstractions am I relying on?

3. Debug Systematically

When debugging, use all three models:

  • Systems thinking: What else is affected?
  • Trade-offs: What constraints led to this design?
  • Abstractions: What’s happening below this layer?

4. Learn from Mistakes

Every bug is a lesson. Ask:

  • Why did this happen?
  • What mental model would have caught it?
  • How can I recognize this pattern earlier?

Putting It All Together

These mental models work together:

Example: Adding a Feature

  1. Systems thinking: How does this affect other components?
  2. Trade-offs: What are we optimizing for? What are we giving up?
  3. Abstractions: What layers are we working with? What might leak?

Example: Debugging a Performance Issue

  1. Systems thinking: Is this a component problem or a system problem?
  2. Trade-offs: Was performance traded for something else?
  3. Abstractions: What’s happening below the abstraction we’re using?

Key Takeaways

  1. Systems thinking: Components interact. Changes cascade. Design for the system.
  2. Trade-offs: Every decision has costs. Make them explicit. Optimize for your context.
  3. Abstractions: They hide complexity but always leak. Understand one level down.

These mental models aren’t rules—they’re frameworks for thinking. The more you use them, the more intuitive they become.

Great engineering intuition comes from recognizing patterns. These three models help you see patterns others miss.

Start applying them today. The next time you’re debugging, designing, or deciding, ask:

  • What systems are involved?
  • What trade-offs are being made?
  • What abstractions are in play?

The answers will guide you to better solutions.

Developing engineering intuition? I mentor engineers on systems thinking, architecture patterns, and building the right mental models. Let's discuss your growth.

P.S. Follow me on Twitter where I share engineering insights, system design patterns, and technical leadership perspectives.

Enjoyed this? Support my work

Buy me a coffee