Ep.2 — A Directory Is a Space
Just as an architect unfurls blueprints to design a building, a programmer designs a directory structure of folders and files. Each directory is like a room, and a well-organized structure—like an architectural blueprint—shapes how easily a project can be maintained and extended.
Just as an architect unfurls a blueprint to design a building, a programmer designs a directory structure made of folders and files. Each directory in the code is like a room in a building, and many directories gathered together can form a vast edifice. A well-organized code structure, like an architectural blueprint, reveals the outline of a project, and this in turn has a major impact on maintenance and extensibility. In this chapter, following the theme that **“a directory is a space,”** I'll unpack software directory structures through architectural metaphors. Let's travel through a world of code structures likened to spaces—starting from a single-room project and reaching all the way to a massive codebase like a sprawling city.
When we first learn programming, we encounter small projects that start with just a few files. The scale is small enough that we feel no problem even with all the code jumbled together in a single folder. But as a project grows and the team expands, the code rapidly becomes complex and the importance of structure comes to the fore. Once it becomes hard to grasp at a glance which file is where, we fall into chaos—like a room with goods piled high as if in a warehouse. This is when systematic organization through a directory structure becomes necessary. A programmer has to divide and name the spaces where code will be placed, and define the role of each space. This resembles an architect's work of designing spaces according to a building's intended uses.
Building a well-crafted directory structure makes maintenance easier even as a project grows in scale. When you want to add a new feature, it's clear where the related code should go, and when you modify an existing feature, you can quickly find which file to touch. The experience that, with a structure in place, you can easily find and edit files no matter how many folders there are, is something many developers relate to. In the end, a sense for directory structure is the ability to view code spatially, and this elevates a programmer's thinking by a level.
Now, let me examine the structure of a single program by likening it to architectural spaces. The various folder structures I'll use as examples are compiled in an appendix at the end of the article, so please follow along and refer to them as needed.
One quiet evening, a junior developer sits at their desk, absorbed in the first program they're building. The project on their computer screen looks like a single small room. Inside the room is one folder, and within it a few files lined up side by side. This tiny room built inside the computer is the first work filled with this junior developer's ideas. In one corner of the room sits a file named main, placed like a desk, and beside it you can see a small drawer chest named helpers. The room may be cramped, but everything you need is within arm's reach. All the code is visible at a glance, and the thoughts in your head correspond directly to the objects (files) in this room.
A project consisting of a single folder is like a studio apartment. When the scale is small, this is actually more efficient. There isn't much moving between files, and since all the logic is gathered in one place, development is fast for a small project. For instance, a simple Python script or an undergraduate-assignment-level program doesn't need to be split into multiple folders—
managing it with just main.py and functions.py is enough. This one-person space has the advantage of being convenient for small-scale projects where development needs to be easy right away.
But once the scale starts to grow, the drawbacks reveal themselves. Just as a single room quickly becomes cluttered when too many belongings pile up, when code swells inside a single folder, maintenance becomes difficult. For one small fix you have to comb through code scattered all over, and unrelated features can end up tangled together. In fact, in the old-style giant single codebases called Monolithic Architecture, even a partial error risked bringing down the entire service, and even a small change required rebuilding and redeploying the whole thing. In other words, there's a risk of ending up with a structure of low cohesion and high coupling. The programmer soon realizes: for a bigger project, you have to divide the room, or build new rooms.
A few days pass, and our developer decides to build a new web application. This time the scale is a bit larger—there are several screens, and there are reusable UI elements too. He conceives the project structure as if designing a house with several rooms. Entering the app through index.js (the entry file), which corresponds to the front door, App.js—the living room—holds the overall structure together. Following the hallway, rooms divided by purpose appear, like a living room, kitchen, and study. To put it in terms of actual folders, the components/ folder holds shared furniture like buttons and input fields used throughout, and the pages/ folder holds page components—Home, About, and so on—each responsible for the role of its own room. On the living room wall hang framed pictures (layout components) called Header and Footer. This house has yet more rooms. In the wardrobe room called styles/ are the CSS files, and in the storeroom called utils/ are various miscellaneous helper functions.
The folder structure recommended for modern frontend projects—especially when using a library like React—is systematic, like a floor plan of a house. As depicted in the scene above, you generally divide and manage folders such as assets (images and fonts), components (reusable UI), pages (page-level components), services (API integration logic), store (global state management), styles (stylesheets), and utils (utility functions). Setting up rooms by role like this makes collaboration convenient too. The designer finds images in assets, the frontend developer finds UI code in components and pages, and the parts that integrate with the backend are checked in services.
By analogy, this structure is a single-room studio expanded into a house where a family lives. Just as you can't cram an entire family's belongings into the living room, when a project grows in scale it's natural to divide rooms by role. Within one room (folder) you keep only the objects (files) essential to that role, and clear boundaries arise between rooms. In the end, this is also a way of practicing the Separation of Concerns. By making each folder responsible only for its own concern (UI components, routing, styles, and so on), the code becomes more readable and easier to manage.
In this way, a component-based frontend structure can be seen as a single house furnished with several rooms. In this house, the developer can move to the appropriate room as needed to get work done. As the project grows, the number of rooms may increase, but the basic principle stays the same: gather related things together in the same space. Do that, and even when the project grows complex, the overall structure is easy to grasp at a glance, and a new developer can look at the map of the house and adapt quickly.
Now the developer takes on a system of even greater scale. This time it's a backend project entangling multiple domains—user management, order processing, payments, and more. He imagines a large office building housing several departments. On the 1st floor is the order-processing department, on the 2nd the membership-management department, on the 3rd the product-management department... The name of each department is posted on the door of its office, and inside are arranged the materials and tools suited to that department's role. For example, the order-processing department's room gathers together a drawer chest for handling order forms (OrderRepository), a counter for taking orders (OrderController), a manual that sets the order rules (OrderService), and so on. Go to the membership-management department's room on another floor, and a member roster (UserEntity), a sign-up form (UserController), member-policy documents (UserService), and the like fill that space. At the center of the building there's also a shared lobby. Like the copier or coffee machine that everyone on every floor uses, it corresponds to the common/ or shared/ folder where code used broadly across the whole project gathers. In this lobby sit, for example, common configuration files, utility functions used by every department, error-handling modules, and so on. Each department focuses on its own work while being able to pull the shared resources it needs from the lobby.
This kind of structure connects with the domain-centric package structure recommended by **Domain-Driven Design (DDD)**. Where the conventional layered structure gathered code by technology (e.g., a controller folder, a service folder, a repository folder), the domain-centric structure gathers code by feature/domain. For example, all code related to users sits under the users/ folder, and order-related code sits under orders/. This way, when a developer modifies or tries to understand a particular feature, they can review all the related content within a single folder without wandering between multiple folders. In fact, there are reports from working developers that after switching to a feature-based (domain-based) package structure, “it became easier to find the class I wanted and the cohesion of the code increased.”
From DDD's perspective, each domain folder holds independence as a **bounded context**. Just as in our metaphor each department's office is an independent space yet collaborates within the building called the company, domain modules likewise interact only through explicit interfaces. Drawing the boundaries clearly like this means a change in one domain (department) has less impact on the other domains, achieving low coupling.
Of course, even when you group code by domain, layered divisions can still exist within each domain. For instance, you might place controller, service, and repository subfolders inside the orders folder. This is like having a reception window (Controller), an office workroom (Service), and a document archive (Repository) even within a department's office. But from the outside, since each floor (domain) is clearly distinguished, grasping the overall structure becomes easier. Lately there's also a growing trend of projects adopting this domain-oriented structure. Through this domain-centric design, our developer neatly organized a complex backend system. Now, even when a new requirement arises, he can ask himself “which domain does this feature belong to?” and then go to the corresponding folder to work. This often brings high cohesion and low coupling, aligning with DDD's core goal of obtaining a design that's easy to change and extend.
Imagine the project grows even larger, until it's split into so many diverse services that they can no longer fit even inside a single building. Our developer takes a step back and gazes at a small city spread out before him. This city is an ecosystem formed by many buildings gathered together. Over there stands the authentication-service building, over here the order-service building stands separately, and beside it you can see the payment-service building. The buildings each have their own unique shape and structure and stand independently, but they're connected by roads and bridges (the API communication network) so that people (data and requests) can come and go. Even if you close the door of one building (one service goes down), the other buildings still carry out their roles. Inside each building, its own electricity and water (database) run separately, so there's a resilience whereby a problem in one doesn't paralyze the entire city.
This small city is precisely a software world implemented with **Microservice Architecture (MSA)**. The features that had previously been gathered in one place are now divided and placed into separate buildings called services. The developer now manages the collaboration of the many buildings through a central coordinator that plays the role of the city hall (e.g., an API Gateway or Service Mesh).
In a microservice structure, the application is split into several independent services (= processes). You could say each one has a self-contained architecture, like a small monolith of its own. Just as the earlier metaphor described each building having its own infrastructure (a database, etc.), in reality microservices often include their own DB. This way, deployment and scaling can be carried out independently per service, maximizing flexibility. You only need to add a new floor (scale out) or do maintenance on the part that needs it, without having to touch the whole thing. The very reason MSA is in the spotlight for large-scale systems is this scalability and reliability.
However, running a city is more complex than managing a single building. With code and data distributed across many places, monitoring becomes difficult, and new types of errors can arise in inter-service communication. Just as traffic control becomes necessary when the road network between buildings grows complex, an MSA environment requires additional management tools and operational cost for logging, tracing, error response, and so on. Also, to use a feature in another service from one service, instead of simply calling a method you have to go through network communication (HTTP requests, etc.), so there's an aspect where development difficulty rises.
From the standpoint of folder structure, microservices commonly take the form of either having per-service folders within a single repository, or running entirely separate repositories per service. In the former case, at the project root you divide folders like account-service/,
order-service/, and payment-service/, and place each service's code inside its respective folder. This composition looks similar on the surface to the earlier domain-based structure, but the scale is larger. That's because there's almost no (or only very limited) code sharing between services, and the deployment unit is each one separately. Recall the metaphor of each being one independent building.
While building this microservice city, our developer is handling both the advantages that clearly delineated separation brings and, at the same time, the challenge of distributed complexity. But he knows this: just as standardized urban planning and administrative management grow more important the larger a city becomes, in a microservice system too it's necessary to keep each service's folder structure in a similar pattern and to establish common conventions. Through this, even when a new service is born, it can be integrated without feeling out of place with the existing city.
Having looked around the city, the developer turns his gaze back to a single building. The structure he'll examine this time is a high-rise building with many layers stacked on top of one another. On the very bottom floor of the building are a storeroom and a machine room, where data and the underlying infrastructure reside. On the floors above is office space where employees handle their work, and on the topmost floor are a reception and information desk that attend to guests. Looking at the building from the side, the divisions between floors are distinct, and the building functions as people and materials move from the lower floors to the upper floors.
This multi-story building is a metaphor for the traditional layered software architecture. One floor is the **Presentation layer**, where the UI and controllers that communicate directly with the user exist. The middle floor is the Application layer or **Domain layer**, where the core business logic runs and the rules are applied. And the lower floor, as the **Infrastructure layer**, is responsible for connections with the database and external systems. In this structure, it's common to have a one-directional dependency in which the upper floors depend on the lower floors, but the lower floors don't need to know about the upper floors. For instance, the domain logic doesn't need to know how the DB is implemented, but the DB layer needs to know the domain in order to store domain objects.
Returning to our building metaphor, even if you change the basement storeroom from MySQL to MongoDB, the work on the building's 1st floor will be less affected—provided the elevator (the inter-layer interface) is properly in place. Also, even if you remodel the interior (UI) of the topmost floor, the office space below it (the business logic) can keep functioning just fine. This is possible because each floor faithfully does only its own role, and interactions happen only through defined routes.
Layered architecture is sometimes contrasted with the domain-based structure or microservices discussed earlier. The domain-based structure and the layered structure are in fact not mutually exclusive concepts. You can apply a layered structure again inside a single domain folder, or conversely have per-layer folders while running domain-based subfolders within each layer. That said, traditionally layered packaging was commonly seen in older monolithic projects above a certain scale, and the industry's experience is that there's a tendency to evolve toward a domain-centric structure as scale gradually grows. Changing from layered to domain-based is similar to remodeling a building's interior to rearrange floors by department. It's hard to overhaul an existing system all at once, but if you gradually demarcate areas and tidy up dependencies, you can ultimately arrive at a structure that's flexible and easy to understand.
Now, let's return once more to the city-scale analogy. This time, imagine a campus-style complex where many buildings are gathered together but exist within a single large wall. Inside this complex stand separate buildings—a research wing, an administration wing, a shared library, a cafeteria—but they're all enclosed by one fence, so access control is unified and resources are shared. A visitor only has to show their ID once at the complex entrance to move freely between the buildings. On the management side too, rather than placing separate security at each individual building, oversight covers the entire complex.
This is the picture that corresponds to a **monorepo** in software development. A monorepo, as the name says, refers to a way of holding multiple projects in a single repository (repo). For example, the frontend, backend, shared libraries, and so on are each separated into folders but live in one repository, with builds and version management done jointly. Tools like Lerna and Yarn Workspaces used in the Node.js ecosystem, or the more recent Turborepo, are representative examples that help operate a monorepo. Just as many teams work within a single campus while sharing common infrastructure, in a monorepo code reuse and avoiding duplication become easier. Multiple packages share the same utilities and configuration, and changes are managed in one place.
The advantage of a monorepo structure is that you can manage everything in one place. The dependency conflicts and version mismatches you might encounter if things were scattered across several small repositories are reduced. Since all packages move within the same release cycle and issue tracker, transparency in collaboration also increases. The buildings within the complex I likened it to earlier communicate closely with one another and share knowledge through the shared library or cafeteria (= the shared folder or common modules). For example, the backend and frontend might both pull and use the same utils functions from the complex's library.
Of course, a monorepo has drawbacks too. Holding everything in one place makes the repository itself large and raises its complexity. The build configuration also has to be elaborately tuned to span multiple projects. It's of a piece with the idea that a well-managed campus is efficient, but if management is poor it can instead become a mess. So when adopting a monorepo, a certain level of automation tooling and team discipline must come along with it. For instance, you need a pipeline that runs the linter and tests across the whole repo, or smart scripts that build only the changed parts.
In terms of folder structure, a typical monorepo takes the form of placing a packages/ folder at the project root and arranging each sub-project (package) as a folder beneath it. You compose it as packages/frontend, packages/backend, packages/shared, and so on. Each package folder is, on its own, a single project structure (a small house or building), while at the root they're bundled together and managed. Thanks to this integrated structure,
code sharing and dependency management take place within a unified context.
Finally, I'd like to close the spatial-metaphor discussion by briefly introducing the actual folder structure of an LLM-based AI project that I, the author, worked on. In this project we developed several services—for example, development proceeded split into two: a chat service and an embedding service. At first I could have managed these as two completely separate projects, but I chose the direction of including both services within a single monorepo. Doing so makes it easy to share the resources commonly used between services.
At the very top of the project, the chat-service/ and embedding-service/ folders sat side by side. The chat-service folder contained the model code and API endpoint implementations needed for generating chat responses, and the embedding-service
folder contained the embedding model code that converts sentences into vectors and the related APIs. These two are small microservices that run independently of each other, but they aren't completely detached. That's because I created a separate space called shared/ for the common resources used by both services. The shared/ folder holds a collection of i18n strings for multilingual support, common configuration files (config), and library code used in common by both services. I thought of this shared folder as something like a shared storeroom or underground passage connecting two buildings. Thanks to it, chat-service and embedding-service could focus on their core features while easily sharing the common data or functionality they needed through shared. The biggest benefit gained through this structure was clear separation and ease of collaboration. If a new team member joins and needs to modify the chat feature, they only have to look at the building called chat-service. Conversely, to change a setting that affects both services, they only have to check one place called shared. Because just looking at a folder's name reveals what role each part plays, understanding the whole project also became easier. In fact, many large-scale projects separate and manage common modules into a shared or common directory, and having practiced this myself, it was certainly effective.
Also, because this project was an AI-model-centered service, putting the different model services into these separated spaces helped the development pipeline as well. The libraries and environment settings each model needed differed slightly, and thanks to separating chat-service and embedding-service, we were able to manage each one's dependencies without conflict. I think it was a good case of practicing microservice separation within a monorepo.
In software design, laying out a code structure isn't merely an administrative task of dividing folders. It closely resembles the creative work of designing spaces in architecture. When we build a small program, we start from a single room, but there we create functions and grow classes, naturally dividing it into sections and defining partitions. The larger the scale grows, the more we come to conceive of an architecture that serves as a blueprint, just as one draws a building's blueprint. The very process of pondering where to place which role of code, how this part and that part will connect, and where to draw boundaries is itself architectural thinking.
The various metaphors examined in this chapter—room, house, office, city, building, campus—are each mirrors reflecting one aspect of software structure. In practice these concepts are sometimes used in combination, and the preferred structure can differ depending on the nature of the project and the team's philosophy. What matters is understanding why you adopt a given structure and being aware of the spatial context in which the code runs. Even a beginner developer, if they make a habit of this kind of thinking starting from small projects, will later be able to exercise an eye for organizing space without panicking even when facing a massive system.
As you learn programming, someday you'll come face to face with complex code. At that moment, try recalling the phrase **“code is architecture.”** Liken the lump of code before you to a building, and imagine a spatial layout for a better design. This is the first step of refactoring, and the start of the road toward becoming a software architect. From single-room code to city-scale systems, if we hold an insight into structure, we'll always be able to build sturdier and more beautiful programs.
(References) Materials I referenced regarding the importance of folder structure in development and various structural patterns: React project example structure fomerain.tistory.com fomerain.tistory.com, domain-driven package design cases mson-it.tistory.com, layered architecture explanations, monolithic vs. microservices comparison sons6488.tistory.com sons6488.tistory.com, the definition of a monorepo, and so on. (You can check the original content via the numbers indicated in the footnotes.)
(I've been busy with product development.. so the series was delayed. My apologies.)