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>
);
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>;
};
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
};
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
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:
- Identify your "hot zones" - components that break most often
- Focus your testing, refactoring, and monitoring on those areas
- 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:
- Start from the beginning: "This function should..."
- Go line by line: "First, we get the user data, then we..."
- 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;
};
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)