Barrel Files (Domain Public API)
Understanding and using barrel files in our DDD architecture.
What are Barrel Files?
A barrel file is an index.ts file that re-exports selected items from other files, serving as a public API for a module or domain.
In Asset360 v3, every domain has a barrel file that defines its public interface:
worker/services/fund/
├── domain.ts # Internal: Types and entities
├── repository.ts # Internal: Data access
├── service.ts # Internal: Business logic
└── index.ts # Public: Barrel file (API) ⭐
Purpose in DDD
Barrel files enforce bounded context boundaries in Domain-Driven Design:
- Define Public API: Explicitly declare what's public vs. internal
- Enforce Encapsulation: Keep implementation details private
- Enable Refactoring: Change internals without affecting consumers
- Prevent Coupling: ESLint enforces barrel imports across domains
Example Barrel File
// worker/services/fund/index.ts
/**
* Fund Domain Public API
*
* This barrel file defines the public interface for the fund domain.
* External domains should only import from this file, not from internal implementation files.
*/
// Domain types
export type {
CreateFundRequest,
CreateFundSnapshotRequest,
EODState,
FundEntity,
FundFeesJson,
FundSnapshotEntity,
ManagementFeeTier,
RunEODRequest,
UpdateFundRequest,
} from "./domain";
// Service
export { FundService } from "./service";
// Repository
export { FundRepository } from "./repository";
How to Use
✅ Correct Usage
Import from the barrel file:
// In worker/services/bank-account/service.ts
import type { FundService, FundEntity } from "../fund";
import type { InvestorService } from "../investor";
import { AggregateAccountingService } from "../accounting";
class BankAccountService {
constructor(
private repository: BankAccountRepository,
private fundService?: FundService,
private accountingService?: AggregateAccountingService,
) {}
}
❌ Incorrect Usage
Direct imports from internal files (ESLint will error):
// ❌ This will fail ESLint
import { FundService } from "../fund/service";
import { FundEntity } from "../fund/domain";
import { FundRepository } from "../fund/repository";
ESLint Error:
Import from domain barrel (index.ts) instead of internal files.
Use "./fund" not "./fund/service" or "./fund/domain".
Internal Imports (Within Same Domain)
Within the same domain, direct imports are allowed:
// In worker/services/fund/service.ts
import type { FundEntity, CreateFundRequest } from "./domain";
import { FundRepository } from "./repository";
// ✅ This is fine - within the same domain
ESLint Enforcement
The project has an ESLint rule (no-restricted-imports) that enforces barrel imports:
// eslint.config.mjs
{
rules: {
'no-restricted-imports': ['error', {
patterns: [{
group: [
'**/services/fund/service',
'**/services/fund/domain',
'./fund/service',
'../fund/service',
// ... all domains
],
message: 'Import from domain barrel (index.ts) instead of internal files.'
}]
}]
}
}
Benefits
1. Clear Public API
Barrel files explicitly declare what's public:
// fund/index.ts - Only these are public
export { FundService } from "./service";
export type { FundEntity } from "./domain";
// fund/domain.ts - These are internal (not exported)
interface InternalFundCalculation {
/* ... */
}
function validateFundCode(code: string) {
/* ... */
}
2. Refactoring Safety
Move or rename internal files without breaking consumers:
// Before: fund/service.ts
// After: fund/fund-service.ts
// barrel file update:
export { FundService } from "./fund-service"; // Changed
// Consumers unchanged:
import { FundService } from "../fund"; // Still works!
3. Prevents Circular Dependencies
Barrel files make import cycles visible and preventable:
// Without barrels - hidden cycle:
// fund/service.ts -> investor/service.ts -> fund/service.ts
// With barrels - cycle is obvious:
// fund/index.ts -> investor/index.ts -> fund/index.ts
4. Better Testing
Mock entire domains at the barrel level:
// test/helpers/mock-services.ts
vi.mock("../worker/services/fund", () => ({
FundService: vi.fn(),
FundRepository: vi.fn(),
}));
5. Documentation
Barrel files serve as documentation of the domain's public API:
// Looking at fund/index.ts tells you:
// - What types are public
// - What services are available
// - What can be used from outside the domain
Trade-offs
Advantages ✅
- Encapsulation: Clear public/private boundaries
- Maintainability: Internal changes don't affect consumers
- DDD Alignment: Enforces bounded contexts
- Tooling Support: ESLint enforcement
Potential Concerns ⚠️
- Bundle Size: Importing one item loads the entire barrel (mitigated by tree-shaking)
- Development Speed: Requires barrel file updates when exposing new exports
- Circular Dependencies: Can hide cycles if not careful (mitigated by explicit exports)
Our Decision
We use barrel files because:
- DDD Requires Boundaries: Bounded contexts need clear APIs
- ESLint Enforcement: Automated checking prevents violations
- Tree-Shaking Works: Modern bundlers optimize barrel imports
- Library Pattern: Matches industry standard (all npm packages use barrels)
Adding a New Domain
When creating a new domain:
-
Create the domain structure:
worker/services/new-domain/
├── domain.ts
├── repository.ts
├── service.ts
└── index.ts ← Create this! -
Populate the barrel file:
// worker/services/new-domain/index.ts
export type { DomainEntity, CreateRequest } from "./domain";
export { DomainService } from "./service";
export { DomainRepository } from "./repository"; -
Add to ESLint rule:
// eslint.config.mjs - add patterns:
'**/services/new-domain/service',
'**/services/new-domain/domain',
'./new-domain/service',
'../new-domain/service', -
Export from central barrel:
// worker/services/index.ts
export { DomainService } from "./new-domain";
export type { DomainEntity } from "./new-domain";
Troubleshooting
Circular Dependency Error
// Error: Circular dependency detected
import { FundService } from "../fund"; // in investor/
import { InvestorService } from "../investor"; // in fund/
Solution: Remove one dependency or introduce a coordinator to orchestrate the interaction.
ESLint Error Not Showing
If ESLint doesn't catch a direct import:
- Check that the pattern is in
eslint.config.mjs - Run
pnpm eslint worker/services/**/*.ts - Restart your editor's TypeScript server
Build Failing After Barrel Update
If the build fails after updating a barrel:
- Verify all exports exist in the source files
- Check for typos in export names
- Run
pnpm typecheckto see specific errors
See Also
- Domain Isolation - Boundary enforcement
- DDD Approach - Domain architecture
- Type Exports - Export best practices
- Project Structure - File organization