This blog post is a translation of a Japanese article posted on April 14th, 2025.
Hi! This is Nakahara from the Sreake team.
As projects grow larger, it becomes difficult to maintain them, which in turn impacts development speed. I believe this largely has to do with a poorly designed directory structure.
Today, I would like to share my thoughts on a directory structure designed to make project maintainance simpler and improve developer productivity.
Some Background
In application development, directory structure has a significant impact on the maintainability, readability, and development efficiency of projects.
Without an appropriate directory structure, the following problems often occur:
- Responsibilities become unclear and code dependencies become complex
- New team members struggle to understand the project, extending onboarding time
- Project management breaks down as the project grows
- Changing code has an oversized impact on unrelated functionality, increasing the cost of introducting new features and fixing bugs
Especially for long-term projects and team-based development, designing an appropriate directory structure is key to success.
What we’re going to cover
This article explains five fundamental principles to consider when designing a directory structure.
By understanding and applying these principles, you’ll be able to:
- Unify development practices across your whole team
- Write better, more readable code, allowing new members to onboard smoothly
- Iterate on and expand your project requirements naturally
- Minimize impact on unrelated functionality when developing new features
By focusing on these five fundamental principles that we’ll introduce, you can improve your directory structure.
The Five Principles
Here are the five principles you should be aware of when designing a directory structure:
| Principle | Overview |
|---|---|
| Separation of Concerns | Organize folders and files by role to clarify responsibilities |
| Scalability | Allow the structure to expand naturally as the project grows |
| Intuitiveness | Strive for intuitive directory design that new members can understand immediately |
| Reusability | Properly separate common logic for efficient reuse |
| Ease of Change | Make it easy to add new features while minimizing impact scope |
Separation of Concerns
When considering directory structure, it’s important that each folder and file has a clear role with properly separated responsibilities.
❌ When responsibilities are ambiguous:
- A single file fills multiple roles, making changes difficult
- The impact of changes becomes hard to predict, leading to bugs
- It becomes difficult to know where specific code is located
✅ When responsibilities are separated:
- One file fills a single, specific role, making changes easy
- The scope of changes is limited, improving maintainability
- Module design becomes more reusable
Bad Example
Multiple responsibilities mixed in a single file:
src/
├── app.ts // Cramming all the logic here
├── user.ts // Mixing UI, API, and business logic
- UI, API, and business logic are in one place, making maintenance difficult
- Roles are unclear, making it hard for others to understand
Good Example
Organized by role:
src/
├── controllers/ // Handles user operations
│ └── userController.ts
├── services/ // Business logic
│ └── userService.ts
├── models/ // Data structure and DB access
│ └── userModel.ts
- Each layer has a clear responsibility and is designed for a single purpose
- The impact scope of changes is limited
Scalability
Directory structures should be designed with consideration for how they can flexibly adapt to project growth.
Even if a simple structure works initially, it can break down as features are added, teams expand, and operational periods lengthen.
❌ Low-scalability structures
- Existing structures must be forcibly modified with each new feature addition
- Files with the same responsibilities are scattered across multiple locations, obscuring the overall picture
- The impact scope of structural changes is large, increasing maintenance costs
✅ Scalable structures
- Structures that make it easy to add or extend features
- Directory structures with consistency that naturally accommodate new elements
- Structures that maintain visibility even as teams and features increase
Bad Example
Flat structure that doesn’t anticipate growth
src/
├── user.ts
├── product.ts
├── cart.ts
- All processes grow independently at the file level
- Becomes chaotic with each new feature addition
Good Example
Layer structure that remains organized for future growth
src/
├── controllers/
│ ├── userController.ts
│ ├── productController.ts
├── services/
│ ├── userService.ts
│ ├── productService.ts
├── models/
│ ├── userModel.ts
│ ├── productModel.ts
- Can naturally expand with the same structure as features increase
- A scalable form that can be introduced from the initial stages
Intuitiveness
Having a directory structure that anyone can easily understand is crucial for team development.
❌ Low readability, non-intuitive structures
- Directory names are ambiguous, making it hard to imagine their contents (e.g.,
common,data,temp, etc.) - Files with the same responsibilities are scattered across multiple locations
- New members easily get lost trying to find “where things are”
✅ High readability and intuitive structures
- Directory and file naming is clear, with roles apparent at a glance
- Folder structure is organized by function and responsibility, making it hard to get lost even for newcomers
- Smooth onboarding is possible because all developers can easily understand the structure
Bad Example
Ambiguous naming and unclear structure
src/
├── stuff/
├── temp/
├── misc.ts
- Roles cannot be imagined from names at all
- Difficult to know where things are, extending onboarding time
Good Example
Names and structure anyone can understand
src/
├── controllers/
├── services/
├── models/
├── utils/
- The purpose of folders is conveyed by naming alone
- Easy to find needed code without getting lost
Reusability
A structure that appropriately extracts and enables reuse of common processes contributes to both development speed and quality.
❌ Low reusability structures
- The same code is written repeatedly for each feature
- Common processes are embedded in specific features, making them difficult to use elsewhere
- Logic duplication creates opportunities for modification errors and bugs
✅ High reusability structures
- Clearly separated reusable areas like
utils/,hooks/,components/,shared/, etc. - Modularized common logic that can be easily called from other features
- Maintenance is easier because the DRY (Don’t Repeat Yourself) principle is thoroughly implemented
Bad Example
Logic duplicated in multiple places
// userController.ts
function formatDate(d: Date) { ... }
// orderController.ts
function formatDate(d: Date) { ... } // Same function reimplemented
Common logic is scattered, requiring updates in all locations when modified
Good Example
Common logic gathered in utils for reuse
src/
├── utils/
│ └── format.ts
├── controllers/
│ └── userController.ts // Uses utils/format
- Same function can be defined and managed in one place
- High reusability, less likely to breed bugs
Ease of Change
A structure that allows smooth addition of new features and modification of existing ones is essential for highly maintainable projects.
❌ Hard-to-change structures
- A single change affects multiple locations
- Feature responsibilities are mixed, making it difficult to determine what to modify
- Hard to test and identify impact scope
✅ Easy-to-change structures
- Features are loosely coupled with localized impact scope
- Easy to test and modify at the module level
- Flexibly adaptable to future feature additions and specification changes
Bad Example
Everything is tightly coupled, with small changes having widespread impact
// app.ts
handleUserLogin()
fetchUserData()
renderUserInfo()
// Everything is unified and difficult to change
Functions are too interdependent, with one change affecting others
Good Example
Concerns are separated, allowing localized changes
src/
├── controllers/
│ └── userController.ts // Handles user input
├── services/
│ └── userService.ts // Logic changes are isolated here
├── models/
│ └── userModel.ts // Data structure modifications are also limited
- Modifications remain localized with minimal impact on other files
- Easier to test and refactor with confidence
Summary
An application’s directory structure is not just a place to put files.
It’s part of a design that directly affects the entire team’s development efficiency, and the project’s maintainability and scalability.
The five fundamental principles introduced in this article are universal concepts that can be applied to any technology stack.
💡 You don’t need to aim for a perfect structure from the beginning.
What’s important is continuously reviewing the structure as the team grows and the project changes.
When thinking about directory structure, aim for design with intention.
This ultimately leads to balancing both development speed and quality.