Why Do We Feel Some Code Is Easier to Read?
Developer Intuition, Code Readability, and Neuroscience

I previously wrote a post called “What Is Good Code? On the Illusion of Readability” where I discussed how subjective and context-dependent the formula “good code = readable code” really is.
The conclusion that people judge readability differently is fair enough. But if you trace the mechanics behind it — where that sense of “this is easy to read” actually comes from — you start to find some hints.
So that’s what this post is about. How humans understand code, and what makes certain forms of information feel easier to process.
What Happens in the Brain When We Read Code
In a 2020 fMRI study conducted at MIT, researchers observed which brain regions activate when programmers read code. The results were interesting.
According to the study, reading code primarily activated the Multiple Demand Network — the region responsible for logical reasoning and complex cognitive tasks — rather than the language network. The language regions weren’t completely uninvolved, but the Multiple Demand Network response was dominant.
Put simply, when our brains read code, they rely more on logical reasoning than language processing.
The study confirmed that five brain regions related to working memory, attention, and language processing showed pronounced activation during code comprehension. Meanwhile, Default Mode Network activity (the regions active when the brain is resting) decreased. In other words, reading code is a fairly expensive operation for the brain.
So far, this might seem unsurprising. But the more interesting findings come from the differences between experts and novices. Skilled developers showed lower overall brain activation levels when reading code. Not because they were trying less, but because they were processing more efficiently. Novice developers, on the other hand, showed widespread brain activation — meaning they were consciously processing nearly everything one piece at a time.
Novices tended to read code like natural language text, top to bottom. Experts followed the program’s execution flow. Same code, fundamentally different processing.
Working Memory: 4 Slots
To understand why some code reads easily while other code refuses to organize itself in your head, you need to know about working memory.
In 1956, cognitive psychologist George A. Miller published the famous paper “The Magical Number Seven, Plus or Minus Two”, revealing that human short-term memory capacity is roughly 7±2 items. Subsequent research has revised this downward — according to Cowan (2001) and others, the number of chunks that can be simultaneously held in working memory converges around 3–4. Though this figure can vary depending on the type of task, level of expertise, and modality of information.
Think about what we load into our heads when reading code. The current value of a variable, the direction of control flow, the order of function calls, the state of the current scope — all of these occupy working memory slots. When those slots fill up, the brain struggles to take in new information. That’s the moment when you get that “this code feels complicated” sensation.
To put it in programming terms, working memory is like a fixed-size stack.
Real working memory is a far more complex system than a stack, but the analogy works well enough for intuiting the key property: “there’s a capacity limit, and exceeding it causes things to break down.” The stack size is roughly 4, and exceeding it causes a stack overflow. In practice, the items occupying each slot aren’t uniform in size, and interference between items also occurs, so the exact number matters less than the fact that capacity is limited. And the moment that capacity hits its limit is exactly when you feel “this code is hard to read.”
Let’s compare code that rapidly exhausts working memory with code that doesn’t.
// Code that rapidly exhausts all 4 working memory slots
function processOrder(order: Order) {
if (order.status === 'pending' && order.items.length > 0) {
const discount = order.customer.tier === 'premium'
? order.items.reduce((sum, item) => sum + item.price, 0) * 0.1
: order.coupon?.discount ?? 0;
const tax = (order.total - discount) * (order.shipping.domestic ? 0.1 : 0);
const finalPrice = order.total - discount + tax + order.shipping.cost;
return { ...order, finalPrice, status: 'processed' };
}
return order;
}This function isn’t wrong, but it demands too many things be held in the reader’s working memory simultaneously.
The condition on order.status, the check on order.items, the discount branching by customer.tier, the null check on the coupon, the domestic/international tax branching, the final price calculation — trying to fit all of this context into 3–4 slots is what makes the brain scream.
If we can break this context into appropriately sized units, we can save working memory slots needed to understand each operation.
// Version that reduces working memory load
function calculateDiscount(order: Order): number {
if (order.customer.tier === 'premium') {
const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
return subtotal * 0.1;
}
return order.coupon?.discount ?? 0;
}
function calculateTax(amount: number, shipping: ShippingInfo): number {
return shipping.domestic ? amount * 0.1 : 0;
}
function processOrder(order: Order) {
if (order.status !== 'pending' || order.items.length === 0) {
return order;
}
const discount = calculateDiscount(order);
const tax = calculateTax(order.total - discount, order.shipping);
const finalPrice = order.total - discount + tax + order.shipping.cost;
return { ...order, finalPrice, status: 'processed' };
}In this version, the amount you need to hold in your head at each step is noticeably reduced. The behavior is identical, but by packaging information into consistent units, we control how much context occupies each working memory slot.
When reading calculateDiscount, you only need to focus on discount logic. When reading processOrder, you can understand the overall flow without knowing each calculation’s implementation details. Each function is digestible within working memory’s capacity, which is why it feels easier to understand. This is also why proper abstraction at appropriate granularity matters in design. Of course, the reverse is also true — splitting things too finely means you spend working memory on jumping between functions and tracking context. The goal isn’t splitting for its own sake, but reducing how much you need to hold in your head at once.
Chunking: The Brain’s Data Compression Algorithm
Here’s a natural question: if working memory only has about 4 slots, how on earth do we comprehend hundreds of lines of code?
The answer lies in a cognitive mechanism called chunking. Chunking means grouping multiple small units of information into a single meaningful cluster. According to research from the University of Zurich, chunking reduces working memory load by retrieving compressed chunk representations from long-term memory to replace individual element representations. The freed-up capacity is then used to process new incoming information.
Think about phone numbers. Trying to remember 01012345678 as eleven individual digits would far exceed working memory capacity. But split it as 010-1234-5678, and you only need to remember three chunks. Better yet, if 010 is already familiar as “Korean mobile prefix,” it’s automatically processed as a single chunk, meaning you really only need to memorize two new chunks.
The same thing happens with code. Look at what happens when a skilled developer reads this:
const activeUsers = users.filter(u => u.isActive).map(u => u.name);A novice developer has to load each individual element into working memory: the users variable, how .filter works, arrow function syntax, the u.isActive property access, how .map works, another arrow function…
For a skilled developer, users.filter(...).map(...) is recognized as a single chunk: “filter array then transform.” Having seen this pattern hundreds or thousands of times, they pull a stored chunk from long-term memory and use just one working memory slot. The remaining slots can be allocated to higher-level thinking like “why is this being filtered?” or “where does the result get used?”
This is the same principle that allows chess masters to glance at a board and grasp the entire situation. Chess masters don’t memorize individual piece positions one by one — they recognize familiar formation patterns as single chunks. That’s why they can quickly recall meaningfully arranged boards but perform no better than novices with randomly placed pieces.
Code works the same way. Idiomatic code is easy to read not because it’s “correct,” but because it matches chunks already stored in the developer’s long-term memory.
And what makes a pattern idiomatic is shaped by external factors: the language’s design intent, standard library conventions, and the community’s repeated choices. It’s not simply “familiar because it’s used a lot” — it’s that convergence driven by multiple forces created that familiarity. This is also why project-specific unique patterns or overly creative code are hard to read. When existing chunks don’t match, the brain has to decompose everything into individual elements for processing, and working memory saturates almost instantly.
System 1 and System 2: Intuition and Analysis
This is where Daniel Kahneman’s dual process theory becomes relevant. Kahneman divided human thinking into two systems:
- System 1: Fast, automatic, intuitive thinking. Based on pattern recognition.
- System 2: Slow, conscious, analytical thinking. Based on logical reasoning.
Most of our everyday judgments are handled by System 1. Driving a car, or sensing your partner’s mood from a single word over the phone — that’s all System 1. When encountering new information, System 1 doesn’t create entirely new patterns; it matches against patterns already stored.
When System 1 gets stuck, that’s when System 2 gets called in. The moment you consciously think “what is this?” and begin analyzing — that’s System 2 engaging. And when you apply this framework to code reading, it explains a remarkable amount.
Readable code is code that’s mostly processed by System 1’s pattern recognition, with minimal System 2 involvement.
When skilled developers read code, familiar patterns are handled automatically by System 1. for loops, if-else branches, map/filter/reduce chaining, try-catch blocks — these have been seen thousands of times, so they’re processed without conscious effort. System 2 stays in a comfortable low-power mode, simply approving the information that System 1 surfaces.
But when an unexpected pattern appears, things change.
// Code that System 1 can handle
const canPurchase = user.age >= 18 && user.isVerified;
// Code that invokes System 2
const canPurchase = !(user.age < 18 || !user.isVerified) && (user.age !== undefined);Both express the same intent. But the moment you see the second version, System 1 sends an “I don’t know” signal, and System 2 begins its expensive analysis. You have to unwind the double negation, run De Morgan’s law in your head, and figure out why that final undefined check is necessary. This transition itself is a cognitive cost.
Kahneman called this phenomenon cognitive strain. The more System 2 intervenes, the more energy the brain consumes and the more fatigue it experiences. The subjective feeling of “hard-to-read code” is really the cognitive cost arising from pattern matching failure triggering a system switch.
From this perspective, feedback like “this is hard to understand” in code reviews isn’t just a matter of taste — it’s closer to a signal that system-switching costs are actually occurring. The magnitude of that cost varies by individual experience and chunk composition, but the fact that the cost occurs is real.
Gestalt Principles: How Visual Structure Affects Code Comprehension
Readable code depends not only on logical structure but on visual structure as well. This is where the perceptual principles of Gestalt psychology come in.
Gestalt psychology studies how the human brain prioritizes perceiving overall patterns and structures over individual elements. Several of its core principles connect directly to code readability.
Law of Proximity
Elements that are close together are perceived as a group. Let’s look at two versions of the same code to see why this grouping matters.
// Code without proximity principle applied
const name = user.firstName + ' ' + user.lastName;
const email = user.email.toLowerCase();
const isValid = email.includes('@') && email.includes('.');
const role = determineRole(user.permissions);
const dashboard = getDashboard(role);
const notifications = getNotifications(user.id, role);// Code with proximity principle applied
const name = user.firstName + ' ' + user.lastName;
const email = user.email.toLowerCase();
const isValid = email.includes('@') && email.includes('.');
const role = determineRole(user.permissions);
const dashboard = getDashboard(role);
const notifications = getNotifications(user.id, role);The second version just adds a single blank line, yet the brain automatically recognizes two groups: “user info processing” and “permission-based data retrieval.”
According to Gestalt research, proximity is an even stronger grouping cue than similarity of color or shape. This principle is used directly in UI design — related information is placed close together and unrelated information is spaced apart, precisely for this reason.
Similarly, the reason blank lines and indentation matter in code isn’t just aesthetics — it’s because the brain’s perceptual system uses them to parse structure.
Law of Similarity
Like proximity, elements that look alike are also perceived as belonging to the same group. This explains why consistent naming matters in code.
// Naming that violates similarity
const userData = fetchUser(id);
const get_orders = retrieveOrderList(userId);
const pmtHistory = loadPayments(uid);// Naming that follows similarity
const user = fetchUser(id);
const orders = fetchOrders(id);
const payments = fetchPayments(id);In the first version, despite all three being the same kind of operation — data fetching — the naming conventions are inconsistent. userData, get_orders, pmtHistory each have different forms, and fetchUser, retrieveOrderList, loadPayments lack any consistency. The brain can’t recognize these as the same group and processes each line as a separate item, consuming working memory.
In the second version, the pattern is clear. The consistent fetch + resourceName structure causes all three lines to be recognized as a single chunk: “data fetching in the same pattern.”
Law of Continuity
The law of continuity states that we perceive elements as a single continuous entity when they follow a natural flow of direction. In code, this relates to the linearity of execution flow.
Our brains find top-to-bottom, left-to-right flow natural. Deep nesting, complex callbacks, and scattered goto statements are hard to read because they violate the law of continuity.
This is also why early return patterns feel easier to read than nested if statements. Once edge cases are filtered out, the remaining code flows in a single direction from top to bottom.
// Code that breaks continuity
function getPrice(user: User, product: Product): number {
if (user.isActive) {
if (product.inStock) {
if (user.tier === 'premium') {
return product.price * 0.8;
} else {
if (product.onSale) {
return product.salePrice;
} else {
return product.price;
}
}
} else {
throw new Error('Out of stock');
}
} else {
throw new Error('Inactive user');
}
}// Code that maintains continuity
function getPrice(user: User, product: Product): number {
if (!user.isActive) throw new Error('Inactive user');
if (!product.inStock) throw new Error('Out of stock');
if (user.tier === 'premium') return product.price * 0.8;
if (product.onSale) return product.salePrice;
return product.price;
}The first version forces your eyes to zigzag right and left as indentation deepens. The brain has to maintain “which condition am I currently inside” in working memory at each nesting level.
The second version filters out exceptions first, then flows naturally from top to bottom. Because it aligns with the law of continuity, the cost for the brain to parse the structure drops significantly.
Cognitive Load Theory: Three Types of Load
There’s a framework that systematically organizes everything discussed so far: John Sweller’s Cognitive Load Theory.
According to this theory, cognitive load during learning or problem-solving breaks down into three types:
- Intrinsic load: The inherent complexity of the task itself. Algorithm difficulty, domain complexity, and so on.
- Extraneous load: Unnecessary complexity from presentation that’s irrelevant to the task. Inconsistent naming, unnecessary indirection, confusing code structure.
- Germane load: The beneficial load spent forming and learning new schemas.
According to this theory, what we should pursue when writing readable code is minimizing extraneous load.
Intrinsic load can’t be reduced — it’s inherent to the problem. Code implementing a distributed consensus algorithm will be complex no matter how well it’s written. The problem to avoid is when extraneous load from “how it’s expressed” piles unnecessarily on top of intrinsic load.
From this perspective, patterns that improve readability — consistent naming, appropriate function decomposition, clear type declarations, meaningful blank lines — are all acts of reducing extraneous cognitive load. They let the brain’s limited resources avoid being wasted on extraneous load and focus on processing intrinsic load (the actual problem).
A 2023 systematic literature review found something interesting as well. Among studies investigating the relationship between source code metrics and actually measured cognitive load, few traditional code metrics showed consistently high correlation with actual cognitive load, and even when correlation was found, results tended to vary depending on task conditions and measurement methods. In other words, what we “measure” as complexity and what a developer’s brain actually “feels” as complex can be different things.
This is a significant finding. Mechanically measurable metrics capture a code’s surface-level complexity, but the cognitive load a developer’s brain actually experiences is influenced by subjective perception — pattern familiarity, chunking efficiency, and visual structural clarity.
Experience Physically Changes the Brain
I mentioned earlier that expert and novice brain activation patterns differ. This isn’t just “more experience means you get better.” A 2024 study published in Scientific Reports measured brainwaves from 62 Python programmers while observing their neural responses to intentionally inserted syntax errors and semantic errors in code.
The results were striking. Skilled programmers showed distinct brainwave patterns for syntactic violations versus semantic violations. This parallels how the brain produces different brainwave responses to grammatical errors versus semantic errors when reading natural language. It means that programming experience forms neural circuits in the brain specialized for processing specific languages and patterns.
In other words, coding experience physically changes the brain’s structure. Through neuroplasticity, code patterns encountered repeatedly are stored as schemas in long-term memory, and these schemas become the foundation for chunking. System 1’s ability to automatically process code is the result of accumulated schemas.
Furthermore, because schemas form based on the patterns an individual has been exposed to, people who repeatedly encounter the same patterns in the same codebase end up sharing similar schemas. Flip this around, and maintaining a consistent code style within a team isn’t a matter of taste — it’s the process of forming shared chunks in team members’ brains. The reason coding conventions matter isn’t just uniformity; it’s collective cognitive efficiency.
Predictive Coding: The Brain Doesn’t Read Code — It Predicts
One of the theories gaining attention in recent cognitive science is predictive coding. According to this theory, the brain doesn’t passively receive information — it constantly predicts what information will come next and only performs additional processing when the actual input differs from its prediction.
The same applies when reading code. Our brains constantly predict what the next line will be.
async function fetchUserProfile(userId: string) {
try {
const response = await api.get(`/users/${userId}`);
// Your brain is already predicting what comes nextA skilled developer’s brain reading this code is already predicting “it’ll probably parse the response data and return it.” And when return response.data; actually appears, the prediction was correct, so virtually no additional cognitive cost is incurred.
But what if something completely unexpected shows up?
async function fetchUserProfile(userId: string) {
try {
const response = await api.get(`/users/${userId}`);
globalEventBus.emit('user-fetched', response.data); // Wait, what?
localStorage.setItem('lastUser', JSON.stringify(response.data)); // Huh?
analytics.track('profile_view', { userId }); // Why is logging here?
return response.data;What the brain predicted from the name fetchUserProfile was “a function that fetches a user profile and returns it.” But then event bus emission, localStorage writes, and analytics tracking appear — actions outside the prediction. Each time, the brain generates a prediction error signal and invokes System 2 to analyze why this code is here.
The reason the second version feels hard to read is that the function is named fetchUserProfile, but internally it does things that have nothing to do with that name. The more closely a name’s set expectation matches the actual behavior, the fewer prediction errors occur and the less cognitive cost is incurred.
This applies not just within functions but to interface design as well. Let me use component interfaces familiar to frontend developers like myself as an example.
// Predictable interface
<TextInput
value={name}
onChange={setName}
placeholder="Enter your name"
/>// Hard-to-predict interface
<UserNameInput
user={name}
setUser={setUser}
blank="Enter your name"
/>The first component follows value, onChange, placeholder — an interface shared by virtually every input component in the React ecosystem. A developer’s System 1, having encountered this pattern countless times, processes it as “it’s an input component” and moves on.
The second uses user, setUser, blank — an interface either tightly coupled to business logic or with unclear meaning. Until you figure out which fields of the user object it touches internally and when blank is actually used, you can’t use it with confidence. You have to look at the internal implementation every time, and each time, prediction errors occur.
In other words, to write readable code, we need to maximize predictability at every level of the codebase — not just function names, but variable names, file structure, directory organization, and API design.
So What Is “Good Code,” Really?
Summarizing everything so far, the substance behind the subjective feeling of “readable code” is this:
- Code that doesn’t exceed working memory: Context that needs to be held simultaneously stays within 3–4 items
- Code that matches existing chunks: Uses familiar patterns so long-term memory schemas can be leveraged
- Code processable by System 1: Understandable through pattern recognition alone, without unnecessarily invoking System 2
- Code whose visual structure matches its logical structure: Aligns with Gestalt principles so the brain’s perceptual system automatically parses the structure
- Predictable code: Doesn’t violate expectations set by names and structure
- Code with low extraneous cognitive load: Removes unnecessary complexity from presentation, leaving only the problem’s inherent complexity
Looking at this list, you’ll notice something interesting. None of these mean “correct code.”
Readability and correctness are separate axes. Code can be perfectly correct but impossible to read, and code can be easy to read but wrong. That said, readable code also makes bugs easier to find — with lower extraneous load, the brain can focus its resources on finding logical errors.
There’s one caveat, though. Kahneman also warned about System 1’s biases. System 1 is fast, but that speed comes at the cost of bias. A classic example is familiarity bias. It’s true that you feel patterns familiar to you as “readable code,” but whether that’s objectively optimal is a separate question. A developer fluent in functional programming might find for loops “hard to read,” and vice versa. In those cases, the feeling of “hard to read” may not reflect the code’s objective quality but the bias of chunks stored in your own System 1.
When giving “this is hard to understand” feedback in code reviews, it’s worth asking yourself once: is this genuinely high-cognitive-load code, or is it simply a pattern not registered in my System 1? If the former, refactoring is needed. If the latter, it might actually be an opportunity to expand your chunk library.
Wrapping Up
“Readable code” isn’t just a matter of feeling or taste. It’s a natural outcome produced by human cognitive architecture — working memory capacity limits, chunking mechanisms, dual process systems, Gestalt perceptual principles, and predictive coding.
The reason I said “readability is subjective” in my previous post comes down to exactly this. Chunking depends on individual experience, and the patterns registered in System 1 differ from person to person.
When two people look at the same code and one finds it easy while the other finds it hard, it’s not just a difference in taste — it’s because the schemas accumulated in their brains are different. The very reason readability is inevitably subjective is embedded in our cognitive structure.
Many developers stop their thinking at “good code is readable code.” But if readable code is the answer, then what exactly is readable code? That’s a question worth asking yourself, and exploring in depth. Stopping your thinking in a vague domain is the most dangerous thing of all.
관련 포스팅 보러가기