DEV Community

Cover image for Mental Models for Developers: A Frontend Perspective
Mmenyene Ndaeyo
Mmenyene Ndaeyo

Posted on

Mental Models for Developers: A Frontend Perspective

Originally presented as a knowledge sharing session to my development team

A little while ago, I was integrating several API endpoints using backend documentation. Most worked perfectly. One kept failing with cryptic error messages. I double-checked the request body, verified headers, tested with Postman, and even compared line-for-line with the working endpoints.

After about two hours of frustration, I reached out to the backend developer. While explaining my request structure, I realized the schema validation was expecting userId as a number, not a string. The next safe action library was silently rejecting the malformed request before it even left the frontend.

The solution took seconds to implement. The hours of debugging could have been possibly avoided if I had approached the problem systematically or with a better mental model.

What Exactly Are Mental Models?

Mental models are cognitive frameworks that help us understand and navigate complex situations. In psychology, they're described as internal representations of how the world works - simplified versions of reality that help us make sense of information and predict outcomes.

For developers, mental models are systematic ways of thinking about code, problems, and solutions. They're the patterns and principles that guide our decision-making before we even write a line of code. While a junior developer might see a bug and immediately start changing variables hoping something works, an experienced developer applies mental models to systematically narrow down the problem space.

Think of mental models as your debugging toolkit for thinking. Just as you wouldn't fix a car engine with only a hammer, you shouldn't approach every coding problem with the same mindset. Different situations require different cognitive approaches.

Consider how you currently approach a new feature request. Do you immediately start coding? Do you break down the problem into smaller pieces? Do you consider what could go wrong? Do you think about how this fits into the larger system? The systematic way you think through these questions - that's your mental model in action.

The mental models I'll share in this article fall into three categories: writing clean code, making smart decisions, and debugging effectively. Each category addresses a different aspect of development thinking, giving you multiple lenses through which to view problems.

Here are seven mental models that have fundamentally changed how I approach development.

Category 1: Writing Clean Code

DRY - Don't Repeat Yourself

The Model: If you find yourself writing similar code more than twice, extract it.

Why it matters: Repeated code creates multiple update points and hiding places for bugs.

The DRY test: Ask yourself, "If I need to change this logic, how many places would I have to update?"

Example:

// Before DRY - Multiple dropdown components
const CountrySelect = ({ value, onChange }) => (
  <div className="form-field">
    <label>Country</label>
    <select value={value} onChange={onChange}>
      {countries.map((country) => (
        <option key={country.code} value={country.code}>
          {country.name}
        </option>
      ))}
    </select>
  </div>
);

const StateSelect = ({ value, onChange }) => (
  <div className="form-field">
    <label>State</label>
    <select value={value} onChange={onChange}>
      {states.map((state) => (
        <option key={state.code} value={state.code}>
          {state.name}
        </option>
      ))}
    </select>
  </div>
);

// After DRY - Generic dropdown component
const SelectField = ({ label, options, value, onChange }) => (
  <div className="form-field">
    <label>{label}</label>
    <select value={value} onChange={onChange}>
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Single Responsibility Principle (SRP)

The Model: Each function, component, or module should do one thing well.

The litmus test: Can you describe what your component does in one sentence without using the word "and"?
Example:

// SRP Violation - component doing too much
const UserDashboard = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    // Fetching user data
    fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
    // AND fetching posts  
    fetch(`/api/posts?user=${userId}`).then(res => res.json()).then(setPosts);
    // AND analytics tracking
    analytics.track('dashboard_viewed', { userId });
  }, [userId]);

  return (
    <div>
      <h1>{user?.name}</h1>
      <div>Posts: {posts.length}</div>
      {/* Rendering user info AND posts */}
    </div>
  );
};

// Better - separated responsibilities
const useUserData = (userId) => { 
  // Custom hook for data fetching only
};

const useAnalytics = (event, data) => { 
  // Custom hook for analytics only
};

const UserDashboard = ({ userId }) => { 
  // Component for rendering only
  const user = useUserData(userId);
  useAnalytics('dashboard_viewed', { userId });

  return <div>{/* Just rendering logic */}</div>;
};
Enter fullscreen mode Exit fullscreen mode

Separation of Concerns

The Model: Keep different types of logic in different places.

In practice: Business logic ≠ UI logic ≠ API logic

Example:

// Mixed concerns - hard to test and maintain
const ShoppingCart = () => {
  const [items, setItems] = useState([]);

  const addItem = async (productId) => {
    // Business logic mixed with API calls mixed with UI updates
    const existingItem = items.find(item => item.id === productId);
    if (existingItem) {
      const updatedItems = items.map(item => 
        item.id === productId ? {...item, quantity: item.quantity + 1} : item
      );
      setItems(updatedItems);
      await fetch('/api/cart', {
        method: 'POST',
        body: JSON.stringify({ items: updatedItems })
      });
      toast.success('Item added to cart!');
    } else {
      // More mixed logic...
    }
  };

  return <div>{/* UI rendering */}</div>;
};

// Separated concerns
const useCartLogic = () => { 
  // Business logic: adding items, calculating totals
};

const useCartAPI = () => { 
  // API operations: sync with server
};

const ShoppingCart = () => { 
  // UI logic: rendering and user interactions
};
Enter fullscreen mode Exit fullscreen mode

Category 2: Smart Development Decisions

YAGNI - You Aren't Gonna Need It

The Model: Don't build features or abstractions until you actually need them.

Important: This is about features, not edge cases. You should still handle null values and empty arrays.

Example:

// YAGNI violation - building for imaginary future needs
const Button = ({ 
  children,
  variant = 'primary',
  size = 'medium', 
  icon,
  iconPosition = 'left',
  loading = false,
  disabled = false,
  tooltip,
  analyticsEvent,
  customTheme,
  hoverAnimation = 'default',
  focusRing = true
  // ... more props "just in case"
}) => {
  // Complex implementation for features nobody asked for
  return (
    <button 
      className={getComplexClassName(variant, size, hoverAnimation)}
      onMouseEnter={() => handleTooltip()}
      onFocus={() => handleFocusRing()}
      // ... lots of conditional logic
    >
      {loading ? <Spinner /> : children}
    </button>
  );
};

// YAGNI applied - start simple
const Button = ({ children, onClick, disabled = false }) => (
  <button onClick={onClick} disabled={disabled}>
    {children}
  </button>
);
// Add complexity only when actually needed
Enter fullscreen mode Exit fullscreen mode

The YAGNI question: "Has someone actually asked for this feature, or am I building it 'just in case'?"

Pareto Principle (80/20 Rule)

The Model: Most of your problems come from a small portion of your code.

In frontend development:

  • Most bugs originate from a few critical components
  • Most performance issues stem from a small subset of your code
  • Most user complaints relate to a handful of features

How to apply it:

  1. Identify your "hot zones" - components that break most often
  2. Focus your testing, refactoring, and monitoring on those areas
  3. Don't spend equal time on all code - prioritize the critical areas

Your checkout flow might represent a small percentage of your codebase but generate most of your bug reports. That's where you invest your testing effort.

Category 3: Debugging & Maintenance

Rubber Duck Debugging

The Model: Explain your code line-by-line to an inanimate object (or person) to find bugs.

Why it works: Forcing yourself to articulate what the code should do often reveals what it actually does.

The process:

  1. Start from the beginning: "This function should..."
  2. Go line by line: "First, we get the user data, then we..."
  3. When you can't explain a line clearly, you've found your bug

This isn't a beginner technique. Senior developers use this regularly because it forces systematic thinking rather than random trial-and-error.

Boy Scout Rule

The Model: Always leave code cleaner than you found it.

Small improvements count:

  • Fix a typo in a comment
  • Extract a magic number to a constant
  • Add a missing TypeScript type
  • Rename a confusing variable

Example:

// You came here to fix a bug in calculateTotal
const calculateTotal = (items) => {
  let t = 0; // <- While fixing the bug, you also improve this
  for (const item of items) {
    t += item.price * item.qty; // <- and this
  }
  return t;
};

// Boy Scout Rule applied
const calculateTotal = (items) => {
  let totalPrice = 0;
  for (const item of items) {
    totalPrice += item.price * item.quantity;
  }
  return totalPrice;
};
Enter fullscreen mode Exit fullscreen mode

The mindset: "I'm here anyway, might as well make it slightly better."

Putting It Into Practice

These mental models aren't rules to follow religiously - they're thinking tools. Start with one:

  • Before writing a component or function, ask: "What is this supposed to do?" (SRP)
  • When you touch existing code, apply the Boy Scout Rule
  • When building features, ask the YAGNI question: "Has someone actually requested this?"
  • When debugging, try explaining the problem out loud before diving into solutions

The goal isn't perfect code; it's better decisions. Mental models help you recognize patterns early, before they become problems.

Next time you're debugging for hours, remember: the issue might not be technical knowledge. It might be thinking systematically about the problem.

Your Turn

Which of these mental models resonates most with your current challenges? Try applying one consciously for a week and notice how it changes your approach to code.

The difference between good developers and great developers often lies not in what they know, but in how they think.

What mental models have shaped your development approach so far? Share your experiences in the comments below.

Top comments (0)