1. Introduction

I’ve spent a long time thinking about what makes code good. To write better code, I’ve studied refactoring, design patterns, various architectures, and development methods. Even when my code worked correctly, I often spent considerable time thinking about better ways to structure or name things.

Yet when it came time to choose between approaches, I still struggled to reach a clear decision. One day I’d implement something one way, then use a different approach the next day. This went on for a long time. Both approaches had clear pros and cons, so either choice left me with some reservations. Here, an “approach” could mean a design pattern or even the name of a function or variable.

At the time, I considered myself a reasonably capable developer. Still, these dilemmas seemed unlikely to go away, no matter how much experience I gained or effort I put in. I even wondered whether such choices belonged to the realm of art, with no right answer and only personal preference to guide them. It felt like a wall I could not get past. Perhaps that thought was a defense mechanism, much like the way developers who don’t understand design patterns dismiss them as useless in practice.

Then I suddenly realized what I had been missing. Let’s look at a few examples to see what it was.

2. Implementing Directional Keys

We need to provide directional keys that let the user choose up, down, left, or right. The four arrows have the same shape and differ only in orientation. There are two ways to implement these keys.

gamepad
Figure 2-1. A gamepad whose directional keys all have the same arrow shape

2.1. Method #1 – Reusing One Image by Rotating It

Method #1 uses a single arrow image (arrow.png), rotated for each direction. Since all four arrows have the same shape, this is easy to implement.

This approach uses less storage space, but the code is less readable.

<Image src="arrow.png" rotate="0" />
<Image src="arrow.png" rotate="180" />
<Image src="arrow.png" rotate="-90" />
<Image src="arrow.png" rotate="90" />

Directional keys using one rotated image
Figure 2-2. Directional keys implemented by rotating a single image

2.2. Method #2 – Using Four Separate Images

Method #2 uses four separate images for up, down, left, and right.

This approach requires more image resources to manage and more storage space. In return, the code is easier to read.

<Image src="up.png" />
<Image src="down.png" />
<Image src="left.png" />
<Image src="right.png" />

Directional keys using four separate images
Figure 2-3. Directional keys implemented with separate images for up, down, left, and right

2.3. Which Method Is Correct?

Which method is correct? Is there even a right or wrong choice here, or does it simply depend on personal philosophy? If you prioritize efficiency, you would choose Method #1; if you prioritize readability, you would choose Method #2.

Should we prioritize readability or performance? Performance may have taken priority in the past, but with today’s more capable hardware, we tend to prefer readable code. Is that reason enough to choose the more readable approach?

There are many considerations, but the first should be what the arrow represents. If its purpose is to point to a particular object, as in Figure 2-4, rotating a single arrow image as in Method #1 is the right approach.

Arrows pointing to objects
Figure 2-4. Arrows used to point to something

However, the user probably had in mind the four fixed arrow keys in the corner of a keyboard, as in Figure 2-5. Using four separate images, as in Method #2, more closely reflects that expectation.

Directional keys with different arrow shapes
Figure 2-5. Directional keys with four distinct arrow shapes

You might think there’s no big difference since the final result the user sees is the same, regardless of which method you choose. But what happens if you ignore the user’s perspective and only focus on implementation convenience?

The user assumes that the arrow shapes can be changed easily at any time. After all, they take it for granted that the directional keys consist of four separate images. Rotating a single image to optimize performance is purely a developer’s concern. One day, the user may casually ask for the keys to look like Figure 2-5. They will expect a simple task: just replace the images. For the developer, however, it becomes a major change to the implementation.

The fact that the arrows have the same shape is simply a coincidence. Building that coincidence into the code moves the implementation further from the user’s understanding and expectations. In other words, ignoring the user’s intent in favor of implementation convenience makes maintenance increasingly difficult.

2.4. The Difficulty of Interpretation

One reason we struggle to choose an implementation when trying to understand the essence of a requirement is that details the user takes for granted are left unstated.

When the user asked for “directional keys,” they probably didn’t specify the arrow keys on a keyboard. From their perspective, that meaning was obvious.

Without that additional information, however, the developer has more to consider when choosing an implementation.

This is what makes interpretation difficult: the developer must fill in the details left undefined because they were considered obvious. Doing so requires understanding both why the requirements were defined that way and how they came to be, which takes considerable experience and insight.

What if you cannot accurately determine the user’s intent at this point? Or predict how the requirements might change?

Define separate Up, Down, Left, and Right components, as shown below, so that changes to the arrow requirements do not affect the rest of the code.

<script>
    const Up = () => <Image src="arrow.png" rotate="0" />
    const Down = () => <Image src="arrow.png" rotate="180" />
    const Left = () => <Image src="arrow.png" rotate="-90" />
    const Right = () => <Image src="arrow.png" rotate="90" />
</script>
<body>
    <Up />
    <Down />
    <Left />
    <Right />
</body>

3. Shallow Routing vs. Nested Routing in REST APIs

Figure 3-1 is a sequence diagram showing how a user selects a movie, theater, and screening date or time in a movie booking service. How should we design the REST API routes for this process?

Figure 3-1

3.1. Shallow Routing

If we design the REST API in the Shallow Routing style, it might look like this:

# Request list of currently showing movies
/movies?status=showing

# Request theaters showing the selected movie
/theaters?movieId={movieId}

# Request list of show dates
/showdates?movieId={movieId}&theaterId={theaterId}

Shallow Routing lets you manage each resource independently, so it’s highly extensible. However, because it doesn’t clearly express relationships between resources, it can be challenging to represent complex hierarchical data.

3.2. Nested Routing

If we design the REST API in the Nested Routing style, it might look like this:

# Request list of currently showing movies
/showing/movies

# Request theaters showing the selected movie
/showing/movies/{movieId}/theaters

# Request list of show dates
/showing/movies/{movieId}/theaters/{theaterId}/showdates

Nested Routing clearly represents the relationships between resources in the URL itself, making it suitable for complex resource structures. However, if the nested resource structure changes, the URL must also change, so it can be less flexible.

3.3. Which Method Is Correct?

We briefly looked at the pros and cons of the two routing methods. So how do you choose between the flexibility of Shallow Routing and the clarity of Nested Routing?

To choose between the two, we need to ask which design better represents the movie booking process at a conceptual level.

From this perspective, Nested Routing directly reflects the movie booking process. Just as the user must select a movie before choosing a theater, the nested route requires a movie to be specified before a theater can be specified. In other words, the REST API mirrors the structure of the movie booking process. This structure alone may be enough to understand the process without separate documentation.

I often see debates about whether Shallow Routing or Nested Routing is better. Such debates are pointless. What matters is which design more accurately reflects the requirements. The debate never ends because the question has no answer from a purely technical perspective.

If you’ve thought about it at length and still can’t find the answer, you’re looking in the wrong place.

3.4. Inheritance vs. Composition

The debate over class inheritance and composition resembles the one over Shallow Routing and Nested Routing.

Just as Shallow Routing is generally considered technically superior because of its flexibility, favoring composition over inheritance wherever possible is often recommended as a better way to reuse code. Here too, however, we should first consider which approach better expresses the domain concepts, rather than prioritize technical superiority.

In the diagram above, a Dog is a kind of Animal, so inheritance naturally expresses that relationship. An Engine, on the other hand, is one of the parts that make up a Car, so composition naturally expresses that relationship.

4. Implementing Support for Documents with Similar Formats

There are two ways to authenticate domestically issued documents, such as income certificates, for use abroad: apostille and consular legalization. Consular legalization is the standard procedure, while the apostille process simplifies it under an international convention.

In a project I worked on, the goal was to encrypt these documents and verify whether they had been tampered with.

Because apostille and consular legalization documents had similar fields and structures, the existing service stored both in a single shared table.

4.1. Initial Design

While analyzing the existing system, I felt that the similarity between apostille and consular legalization documents was merely a coincidence, and that they should not be treated as the same document type. If they were the same, the project would not have been called “Apostille & Consular Legalization.”

The backend developer, however, argued that there was no need to separate them. We eventually compromised on two separate REST APIs backed by a shared service and table.

4.2. Revising the Design

As the project progressed, the differences between the two document types became clearer. Apostille and consular legalization documents could have overlapping document numbers, so the numbering scheme had to change. As the service gained more features, the interfaces for the two document types also grew further apart.

We eventually decided to use separate tables and separate internal implementations. Fortunately, the public APIs were already separate, so changing the internal structure was relatively straightforward. Had we avoided refactoring because separating the implementations felt too burdensome, the code would have filled with if-else statements, opening the gates of hell.

4.3. Why Did This Happen?

In this case, the matching document formats were simply a coincidence. They could change in any number of ways as user requirements changed. The problem was overlooking the fact that the documents had different names because they were different documents to begin with.

Programmers often prioritize implementation convenience, and that habit can be hard to break. Still, the implementation should faithfully reflect the domain concepts.

5. Storing Encoded Filenames

Suppose a user wants to upload a file named 한글.txt via a web browser.

Because the filename contains non-ASCII characters (Korean characters), it must be URL-encoded when sent to the server. Likewise, the filename must be URL-encoded when the user downloads the file.

Should the server store the encoded string (%ED%95%9C%EA%B8%80.txt) in the database as received? Or should it decode the string and store it as 한글.txt?

If you store it as 한글.txt, you’ll have to encode it again when sending it back to the user for download. Isn’t it more efficient to just store it as %ED%95%9C%EA%B8%80.txt?

To understand the essence, consider the user’s perspective. The name of the file they uploaded is 한글.txt. They do not expect it to be transformed into something else. The right choice is therefore to store it as 한글.txt, matching their understanding.

URL encoding is needed because of the limitations of the ASCII character set; it is not a user requirement. Allowing the limitations or characteristics of one technology to affect other parts of the system is poor design. Technical issues that arise during HTTP transmission should be resolved during transmission. Carrying them into the database creates an antipattern: tight coupling between two major parts of the system. Accurately reflecting the user’s intent comes first; optimization comes afterward.

If downloading were the only feature to consider, storing the filename as received would be the best choice. But as the system gains features such as file listings or search, it will need the original string (한글.txt), because that is how the user understands the filename. Storing it as %ED%95%9C%EA%B8%80.txt would make those features harder to implement.

When we prioritize implementation convenience, even small changes can disrupt the design. Understanding the essence and letting it guide the implementation helps us respond more easily to changes we did not anticipate.

// Will you display encoded filenames?
%ED%95%9C%EA%B8%80.txt
%ED%85%8C%EC%8A%A4%ED%8A%B8.jpg
%ED%8C%8C%EC%9D%BC.json

// Or will you display original filenames?
한글.txt
테스트.jpg
파일.json

6. Conclusion

The examples share a common focus on “why” rather than “what.” The “what” is just one means of achieving the “why.” The purpose (why) does not change easily, but the method (what) can change in many ways as circumstances change.

Another important reason to focus on “why” is that we cannot document every thought the user has during requirements analysis. The same is true of design: we cannot capture everything in the designer’s mind. The resulting code must reflect these incomplete requirements and designs as faithfully as possible. Some gaps are inevitable, often involving details people assume will be understood without being stated. The problem is that developers may interpret what users take for granted in an entirely different way.

Focusing on “why,” however, helps users and developers look in the same direction. Even when communication leaves some gaps, their interpretations are less likely to diverge significantly. Reducing these differences in understanding is one of the important roles of Essence-Based Interpretation (EBI).

EBI is such a basic, seemingly self-evident principle that it is difficult to define its scope or specific ways to put it into practice. It is not limited to software development, either.

EBI shares common ground with Domain-Driven Design (DDD) in emphasizing that development should be grounded in the domain. DDD is a design methodology that systematically addresses domain complexity through strategic and tactical patterns such as bounded contexts, ubiquitous language, and aggregates. DDD also depends on a deep understanding of the domain’s essence, so the two concepts are complementary rather than opposed.

The difference lies in the scope and depth of their concerns. DDD provides concrete tools and patterns for structuring domain models, while EBI operates at an earlier stage. By understanding why requirements were defined as they were, rather than focusing on their surface form, EBI establishes criteria for choosing an implementation approach. It is a general way of thinking that can be applied across many fields, including software development, rather than a specific methodology.

Once understood, EBI can sound obvious. Giving it a grand name like “Essence-Based Interpretation” therefore feels a little embarrassing. Still, I want to define it explicitly in the hope that doing so helps developers, myself included, become more conscious of this idea.

The pursuit of good code changes how we think. That shift leads to a strategy for handling unpredictable change by understanding the essence of what we are building. This is a rewarding challenge unique to software development.