I ran into a Circular Dependency error in a project.

Tracing the cause revealed that the culprit was the barrel file. A barrel file is an index.ts that re-exports multiple modules from a folder.
After realizing the root cause, I decided to remove the barrel-file pattern from the project. I want to document the positive outcomes I gained and, despite those, the cases where I chose to keep barrels.
I just wanted prettier imports
The reason I introduced barrel files was simple: I wanted clean-looking import statements.
// Rather than this
import { validateEmail } from "@/shared/helpers/validation/email";
import { formatDate } from "@/shared/helpers/format/date";
import { calculateAge } from "@/shared/helpers/calculate/age";
// this looks nicer!
import { validateEmail, formatDate, calculateAge } from "@/shared/helpers";I didn’t think about side effects. My thinking was, “Isn’t it better if I can shrink it to one line?” Since many projects were already using barrel files, I didn’t even suspect it might be a bad pattern.
Then came a circular dependency!
One day I eventually hit a circular dependency error. The problem looked like this:
// 1. Inside some file under shared/helpers
import { SnsPlatforms } from "@/shared/enums";
// 2. Inside the SnsPlatforms file
// @/shared/enums/SnsPlatforms.ts
import { createI18nMap } from "@/shared/helpers";
// Problem: circular dependency caused by the barrel
// shared/helpers/index.ts gathers all exports, so
// shared/helpers → shared/enums → shared/helpers becomes a cycle
// Temporary fix: import directly without going through the barrel
import { createI18nMap } from "@/shared/helpers/enum";Because the barrel collected all exports in one place, circular dependencies occurred easily among mutually dependent files.
This experience prompted me to reconsider barrel files. I found plenty of posts already pointing out their downsides:
Circular deps are only part of it…
Using barrel files introduces more problems than just circular dependencies.
Tree-shaking issues
Modern JS bundlers (Vite, Webpack, etc.) perform tree-shaking to eliminate unused code. Barrel files can get in the way of this process.
// 1. Using a barrel (Bundler: "I need to analyze index.ts.")
import { validateEmail } from "@/shared/helpers";
// 2. Importing directly (Bundler: "I only need to analyze email.ts!")
import { validateEmail } from "@/shared/helpers/validation/email";In case 1, to figure out whether only validateEmail is needed, the bundler has to analyze the entire @/shared/helpers (index.ts). If that index.ts re-exports dozens of files, the bundler may conservatively conclude, “Some of these might have side effects; better include them all in the bundle.”
As a result, even though you only needed validateEmail, the final bundle could end up containing every utility in the helpers folder.
So I removed the barrels
After that incident, I decided to remove all barrel files. As I did so, I had several positive experiences:
1. Imports are still neat
This was my initial worry. The import block did get a few lines longer, but it wasn’t bothersome. Since IDEs can fold import blocks anyway, it wasn’t a practical issue.
In fact, imports became more explicit about where things come from, making code reading much easier. @/shared/helpers is vague, whereas @/shared/helpers/format/date expresses intent clearly.
2. Unnecessary folders/files disappeared
Using barrels gave me this implicit compulsion: “If there’s a folder, there must be an index.ts.” That led to inefficient structures like this where a folder had a single file plus an index for re-exporting:
features/
└── helpers
└── something.ts
└── index.tsAfter removing barrels, those unnecessary index.ts files disappeared, and the structure became simpler and more intuitive.
3. Fewer human “oops” errors
With barrels, every new util or component required edits in two places:
- Create the actual file (e.g.,
new-util.ts) - Add an export to the barrel (
index.ts)
When you’re busy touching multiple files, step 2 is easy to forget. Now, as soon as I create a file, I can import it elsewhere directly by its path.
4. Refactoring got much easier
When files were tied together by an index, structural changes were cumbersome. I had to adjust the index as well, and sometimes the IDE didn’t catch all the updates, so I had to fix paths manually. Now that nothing is tethered to an index, the IDE updates import paths automatically, making refactors much more fluid.
Barrel files aren’t pure evil
I didn’t delete every barrel file. I kept them where there was a clear purpose, such as:
- Folders like Toast, where the public interface should be limited to specific exports like
useToast,ToastProvider, etc., regardless of internal files - Component groups like Table that are split into
Row,Cell,Header, but are typically imported together to assemble a fullTable
components/
Table/
index.ts # keep the barrel
Table.tsx
TableRow.tsx
TableCell.tsx
TableHeader.tsxTakeaway
Every approach has pros and cons. Barrel files are no different. Rather than a “must use always,” think of them as an option to choose when appropriate. What matters is understanding why you chose them and staying flexible enough to try alternatives when problems arise.