Loading learning content...
We've explored the principles of code organization—layer vs. feature, coupling and cohesion, module boundaries. But principles are only valuable when they translate into actionable decisions. When you create a new project or join an existing team, you need concrete patterns that embody these principles.
This page provides practical organization strategies you can apply directly. These aren't theoretical ideals—they're battle-tested structures used in production systems across startups and enterprises. We'll examine patterns for different scales, from small projects to large distributed teams, and provide specific guidance on evolving your structure as your system grows.
By the end of this page, you'll have a toolkit of organization patterns you can apply to projects of any size. You'll understand when to use each pattern, how to evolve from simpler to more sophisticated structures, and how to handle the practical challenges that arise in real-world codebases.
A critical insight for practical organization is that structure should evolve with complexity. The right structure for a 3-month MVP is different from the right structure for a 3-year-old product with 50 contributors.
Many teams fail because they either:
Instead, plan for evolution. Here's a typical progression:
| Stage | Team Size | Codebase Size | Recommended Structure | Key Focus |
|---|---|---|---|---|
| Prototype | 1-2 devs | <10k lines | Flat/Simple layer-based | Speed to validate idea |
| Early Product | 3-5 devs | 10-50k lines | Feature-based with light layering | Establish conventions |
| Growing Product | 5-15 devs | 50-200k lines | Full feature-based with internal layers | Team autonomy, clear boundaries |
| Mature Product | 15-50 devs | 200k-1M lines | Modular monolith or distributed | Independent deployability |
| Enterprise Scale | 50+ devs | 1M+ lines | Microservices or federated modules | Organizational scaling |
Don't view restructuring as failure—it's a sign of growth. The goal isn't to predict the perfect structure on day one (impossible), but to establish strong boundaries that make restructuring feasible. If you've maintained loose coupling and clear contracts, restructuring is surgical. If you've let everything couple to everything, restructuring requires a rewrite.
This pattern suits early-stage products with a small team. It provides feature isolation without over-engineering. Each feature is a folder containing all related code.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
src/├── features/│ ├── authentication/│ │ ├── login.controller.ts│ │ ├── login.service.ts│ │ ├── login.dto.ts│ │ ├── user.entity.ts│ │ ├── user.repository.ts│ │ └── auth.routes.ts│ ││ ├── products/│ │ ├── product.controller.ts│ │ ├── product.service.ts│ │ ├── product.dto.ts│ │ ├── product.entity.ts│ │ ├── product.repository.ts│ │ └── product.routes.ts│ ││ └── orders/│ ├── order.controller.ts│ ├── order.service.ts│ ├── order.dto.ts│ ├── order.entity.ts│ ├── order.repository.ts│ ├── order-item.entity.ts│ └── order.routes.ts│├── shared/│ ├── database/│ │ ├── connection.ts│ │ └── base-repository.ts│ ├── middleware/│ │ ├── auth.middleware.ts│ │ └── error-handler.middleware.ts│ └── utils/│ ├── validation.ts│ └── formatting.ts│├── config/│ ├── database.config.ts│ ├── app.config.ts│ └── env.ts│└── main.tsNote the consistent naming pattern: {entity}.{type}.ts. This convention (product.controller.ts, product.service.ts) makes files easy to find and creates predictable imports. Establish naming conventions early—they're hard to change later.
As features grow complex, introduce internal layering within each feature module. This pattern combines feature-based top-level organization with Clean/Hexagonal architecture principles internally.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
src/├── modules/│ ├── user-management/│ │ ├── api/ # Presentation Layer│ │ │ ├── controllers/│ │ │ │ ├── UserController.ts│ │ │ │ └── AuthController.ts│ │ │ ├── dtos/│ │ │ │ ├── CreateUserRequest.ts│ │ │ │ └── UserResponse.ts│ │ │ └── routes.ts│ │ ││ │ ├── application/ # Application Layer (Use Cases)│ │ │ ├── commands/│ │ │ │ ├── RegisterUserCommand.ts│ │ │ │ └── UpdateProfileCommand.ts│ │ │ ├── queries/│ │ │ │ ├── GetUserByIdQuery.ts│ │ │ │ └── SearchUsersQuery.ts│ │ │ └── handlers/│ │ │ ├── RegisterUserHandler.ts│ │ │ └── GetUserByIdHandler.ts│ │ ││ │ ├── domain/ # Domain Layer (Core Business Logic)│ │ │ ├── entities/│ │ │ │ ├── User.ts│ │ │ │ └── UserProfile.ts│ │ │ ├── value-objects/│ │ │ │ ├── Email.ts│ │ │ │ ├── Password.ts│ │ │ │ └── UserId.ts│ │ │ ├── repositories/│ │ │ │ └── UserRepository.ts # Interface only│ │ │ └── services/│ │ │ └── PasswordHasher.ts # Interface for domain service│ │ ││ │ ├── infrastructure/ # Infrastructure Layer│ │ │ ├── persistence/│ │ │ │ ├── PostgresUserRepository.ts│ │ │ │ └── UserMapper.ts # DB <-> Domain mapping│ │ │ ├── services/│ │ │ │ └── BcryptPasswordHasher.ts│ │ │ └── external/│ │ │ └── EmailVerificationService.ts│ │ ││ │ └── index.ts # Module's public API│ ││ ├── order-processing/│ │ ├── api/│ │ ├── application/│ │ ├── domain/│ │ ├── infrastructure/│ │ └── index.ts│ ││ └── payment/│ ├── api/│ ├── application/│ ├── domain/│ ├── infrastructure/│ └── index.ts│├── shared-kernel/ # Shared Domain Concepts│ ├── domain/│ │ ├── Money.ts│ │ ├── Address.ts│ │ └── AuditInfo.ts│ └── events/│ ├── DomainEvent.ts│ └── EventBus.ts│├── infrastructure/ # Application-Wide Infrastructure│ ├── database/│ │ └── connection.ts│ ├── messaging/│ │ └── rabbitmq-client.ts│ └── logging/│ └── logger.ts│└── main.tsThe Modular Monolith pattern treats each module as if it could be a separate service—but deploys everything together. This provides the organizational benefits of microservices (team autonomy, clear boundaries, independent development) without the operational complexity of distributed systems.
This pattern is increasingly popular as organizations recognize that microservices impose significant overhead and isn't always the right choice.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
monorepo/├── packages/ # Each module is an npm/Maven/NuGet package│ ├── user-management/│ │ ├── package.json # Module dependencies explicit│ │ ├── src/│ │ │ ├── api/│ │ │ ├── application/│ │ │ ├── domain/│ │ │ ├── infrastructure/│ │ │ └── index.ts│ │ └── tests/│ │ ├── unit/│ │ ├── integration/│ │ └── contract/ # Contract tests for public API│ ││ ├── order-processing/│ │ ├── package.json│ │ ├── src/│ │ │ ├── api/│ │ │ ├── application/│ │ │ ├── domain/│ │ │ ├── infrastructure/│ │ │ └── index.ts│ │ └── tests/│ ││ ├── payment/│ │ ├── package.json│ │ ├── src/│ │ └── tests/│ ││ └── shared-kernel/ # Shared domain package│ ├── package.json│ ├── src/│ │ ├── domain/│ │ └── events/│ └── tests/│├── apps/ # Deployable applications│ ├── main-api/ # Composes modules into API│ │ ├── package.json # Depends on all module packages│ │ └── src/│ │ ├── composition-root.ts # Wires all modules together│ │ ├── routes.ts│ │ └── main.ts│ ││ └── admin-api/ # Different composition for admin│ ├── package.json│ └── src/│├── infrastructure/ # Shared infrastructure package│ ├── web-framework/│ ├── persistence/│ └── messaging/│└── package.json # Workspace rootMany successful companies (Shopify, Segment) advocate starting with a modular monolith and only extracting services when there's a clear operational need (independent scaling, different technology, team ownership mandates). This approach gives you architectural discipline without premature distribution complexity.
Every organization strategy must address cross-cutting concerns—functionality that spans multiple features or layers. Logging, authentication, validation, error handling, and monitoring don't belong to any single feature.
Handling cross-cuts poorly leads to either:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// infrastructure/cross-cuts/LoggingDecorator.ts// Cross-cutting concern: Method execution loggingexport function LogExecution(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = async function(...args: any[]) { const logger = getLogger(target.constructor.name); const start = Date.now(); logger.debug(`Executing ${propertyKey}`, { args }); try { const result = await originalMethod.apply(this, args); logger.debug(`Completed ${propertyKey}`, { durationMs: Date.now() - start }); return result; } catch (error) { logger.error(`Failed ${propertyKey}`, { error, durationMs: Date.now() - start }); throw error; } }; return descriptor;} // modules/order-processing/application/handlers/CreateOrderHandler.ts// Feature code is clean - cross-cuts applied via decoratorsexport class CreateOrderHandler { constructor( private orderRepository: OrderRepository, private paymentService: PaymentProcessor, // Injected abstraction private eventBus: EventBus, // Injected infrastructure ) {} @LogExecution // Cross-cut: logging @Transactional // Cross-cut: transaction management @RequireAuth('order:create') // Cross-cut: authorization async handle(command: CreateOrderCommand): Promise<OrderId> { // Feature logic only - no logging, auth, or tx code here const order = Order.create(command.items, command.customerId); await this.paymentService.authorize(command.payment); await this.orderRepository.save(order); await this.eventBus.publish(new OrderCreated(order)); return order.id; }}Some cross-cuts have domain implications—auditing, authorization with business rules, data validation. These belong closer to the domain, not in generic infrastructure. Authorization that checks 'user owns this order' is domain logic, not infrastructure. Place it accordingly.
Your organization structure directly impacts your testing strategy. Well-structured modules are inherently more testable; poorly structured code fights testing at every level.
Here's how the patterns align with testing approaches:
| Layer | Test Type | What to Test | Mocks/Stubs |
|---|---|---|---|
| Domain | Unit tests | Business rules, invariants, calculations | None (domain is pure) |
| Application | Unit/Integration | Use case flows, orchestration logic | Mock infrastructure interfaces |
| Infrastructure | Integration tests | Database access, external services | Real deps or test containers |
| API | Contract/E2E tests | Request/response contracts, routing | Full stack or mock services |
| Cross-Module | Integration/E2E | Module interactions, event flows | Depends on coupling level |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
modules/├── order-processing/│ ├── src/│ │ ├── domain/│ │ │ └── Order.ts│ │ ├── application/│ │ │ └── CreateOrderHandler.ts│ │ └── infrastructure/│ │ └── PostgresOrderRepository.ts│ ││ └── __tests__/│ ├── domain/│ │ └── Order.test.ts # Pure unit tests - no mocks needed│ │ │ ├── application/│ │ └── CreateOrderHandler.test.ts # Unit tests with mocked infrastructure│ ││ ├── infrastructure/│ │ └── PostgresOrderRepository.integration.test.ts # Real database│ ││ └── contract/│ └── OrderModule.contract.test.ts # Contract tests for public API // Domain test example - pure, fast, no dependenciesdescribe('Order', () => { it('should calculate total correctly', () => { const order = Order.create([ OrderItem.create(ProductId.new(), Money.usd(10), Quantity.of(2)), OrderItem.create(ProductId.new(), Money.usd(15), Quantity.of(1)), ]); expect(order.total).toEqual(Money.usd(35)); }); it('should reject empty orders', () => { expect(() => Order.create([])).toThrow(EmptyOrderError); });}); // Application test - mocked infrastructuredescribe('CreateOrderHandler', () => { it('should create order and publish event', async () => { const mockRepo = { save: jest.fn() }; const mockEventBus = { publish: jest.fn() }; const handler = new CreateOrderHandler(mockRepo, mockEventBus); await handler.handle(new CreateOrderCommand(/* ... */)); expect(mockRepo.save).toHaveBeenCalledWith(expect.any(Order)); expect(mockEventBus.publish).toHaveBeenCalledWith(expect.any(OrderCreated)); });});A well-structured codebase enables a healthy testing pyramid: • Many fast domain unit tests (base of pyramid) • Moderate application layer tests with mocked infra • Few slow infrastructure integration tests • Minimal E2E tests for critical paths
If your pyramid is inverted (mostly E2E, few unit tests), your structure may be too coupled.
Most developers don't start greenfield projects—they inherit codebases that evolved organically over years. Restructuring such codebases requires careful strategy to avoid breaking changes and maintain velocity during transition.
123456789101112131415161718192021222324
src/├── controllers/│ ├── UserController.ts│ ├── OrderController.ts│ └── ProductController.ts├── services/│ ├── UserService.ts│ ├── OrderService.ts│ └── ProductService.ts├── repositories/│ ├── UserRepository.ts│ ├── OrderRepository.ts│ └── ProductRepository.ts├── models/│ ├── User.ts│ ├── Order.ts│ └── Product.ts└── utils/ └── helpers.ts Issues:• Feature code scattered• No domain isolation• Everything accesses everything12345678910111213141516171819202122232425
src/├── modules/│ ├── user-management/│ │ ├── api/│ │ │ └── UserController.ts│ │ ├── application/│ │ │ └── UserService.ts│ │ ├── domain/│ │ │ └── User.ts│ │ └── infrastructure/│ │ └── UserRepository.ts│ ├── order-processing/│ │ ├── api/│ │ ├── application/│ │ ├── domain/│ │ └── infrastructure/│ └── product-catalog/│ └── ...└── shared/ └── ... Improvements:• Feature cohesion• Clear layer boundaries• Dependencies explicitLarge codebase restructuring takes months, not weeks. Plan for incremental progress alongside feature work. The goal is continuous improvement—don't let perfect be the enemy of better. Every file you move to the new structure is a step forward.
Let's consolidate the practical patterns and principles for organizing code effectively:
Module Complete:
This module has covered the full spectrum of package and module organization—from the fundamental choice between layer-based and feature-based organization, through coupling and cohesion metrics, to boundary definition and practical implementation patterns.
The key insight is that organization is not aesthetic—it's strategic. How you structure code determines how easily teams collaborate, how quickly features ship, how gracefully the system evolves, and ultimately, how sustainable your velocity remains over years of development.
Apply these principles progressively. Start with clear feature boundaries and explicit contracts. Add internal layering as features grow. Evolve toward modular monolith as scale demands. Let structure serve your needs, not constrain them.
You now have a comprehensive toolkit for organizing code at the package and module level. You understand both principles and practical patterns, can evaluate existing codebases for structural health, and have strategies for evolving structure as systems grow. Apply these concepts to your projects, and watch maintainability and team productivity improve.