Skip to content

Instantly share code, notes, and snippets.

@ewaldbenes
Last active January 26, 2026 19:33
Show Gist options
  • Select an option

  • Save ewaldbenes/a7879a187cedb47ed9744ad2929e5d79 to your computer and use it in GitHub Desktop.

Select an option

Save ewaldbenes/a7879a187cedb47ed9744ad2929e5d79 to your computer and use it in GitHub Desktop.

Architecture: Modular Monolith Status: Production_Ready License: MIT

The Ultimate Project Layout Reference

This reference outlines a robust file system layout for enterprise-grade software projects. It provides a blueprint for structuring source code to achieve a modular, maintainable architecture.

The layout accounts for complex requirements such as multiple transport types (HTTP, WebSockets, CLI), varied data encodings, and multi-tenancy configurations where specific clients require custom database extensions. While few projects require every directory presented here simultaneously, this guide serves as a comprehensive map for placing code and resources correctly. It also guides you on how to scale down the layout for smaller projects below the full layout.

Language Agnostic: While this reference uses TypeScript (.ts) for demonstration, the architecture is independent of any specific programming language. It relies on structural concepts that transcend syntax, making it equally applicable to Python, Go, Java, or polyglot environments.

Why This Exists

I follow Domain-Driven Design and functional programming principles for almost all my projects. I have found that it pays off even for small, one-time scripts. For example, I once wrote a 600-line Python script to transform a LimeSurvey export (*.lsa); by separating the I/O from the logic, the script became much easier to handle.

But for every new project, I faced the same problem:

Where should I put the files?

Thinking about project structure while trying to code adds a mental burden I wanted to avoid. I spent years searching for a "good" structure, but existing boilerplates never quite fit my specific needs. I couldn't find a layout generic enough to handle every edge case.

Therefore, I compiled the lessons from all my projects into this single, unified layout. It scales to fit any dimension or requirement I throw at it.

I hope you find it useful.

Layout Philosophy

  • Universal: Applicable to any domain, regardless of scale.
  • Unidirectional: The direction of dependencies (who imports whom) is constant.
  • Functional Core: Algorithms are isolated from side effects (I/O).
  • Testability First: It sacrifices physical cohesion to guarantee 100% testable logic.
  • Extensible: Built on a pattern of "Shared Defaults" with "Specific Overrides."
  • Granular Lifecycle: It separates code (and data) based on when and why it changes.

Layout

.
├── css                                     // Main stylesheet, populated by Tailwind CLI
│   └── style.css
├── db                                      // Database layout in plain SQL (Separated from source code)
│   ├── core                                // Base schema shared across all environments; supports multiple DBs per Bounded Context
│   │   ├── cache                           // Ephemeral data (Re-creatable/Disposable)
│   │   │   └── migrations                  // e.g., LLM response caches, session lookups
│   │   ├── var                             // Persistent application state (Source of Truth)
│   │   │   ├── migrations                  // e.g., User records, transaction history
│   │   │   └── seeds
│   │   │       └── user-roles.sql
│   │   └── tmp                             // Volatile state (Seconds/Minutes)
│   │       └── migrations                  // e.g., Throttling counters, temporary locks, offline lock pattern
│   ├── environments                        // Environment-specific schema extensions
│   │   ├── project-E
│   │   │   ├── migrations                  // Extension tables specific to Project E
│   │   │   │   └── 001_project_e_extensions.sql
│   │   │   └── seeds
│   │   │       └── default_admin_user.sql
│   │   └── customer-F
│   │       ├── migrations
│   │       └── seeds
│   └── scripts                             // DB Maintenance (backup, restore, sanitize)
├── scripts                                 // DevOps and automation scripts (bash/python/ts)
│   └── ...
├── src
│   ├── index.ts                            // Runtime Dispatcher (Selects project entry point; discouraged due to lack of process isolation)
│   ├── environments                        // Composition Roots (One folder per client/tenant/dev config); depends on Infrastructure and Core
│   │   ├── shared                          // Cross-configuration library (Configuration logic too specific for Core)
│   │   │   └── templates                   // Templates used across all environments
│   │   │       ├── sms-notification.eta
│   │   │       ├── email-body-text.eta
│   │   │       └── email-body-html.eta
│   │   ├── project-E
│   │   │   ├── delivery                    // Project-specific UI/Protocol overrides
│   │   │   │   ├── templates               // Project E's unique Eta templates
│   │   │   │   ├── http                    // Project E's specific route definitions
│   │   │   │   │   ├── controllers
│   │   │   │   │   └── route.ts
│   │   │   │   └── websocket
│   │   │   ├── env.ts                      // Project E's configuration implementation
│   │   │   └── index.ts                    // Wiring: Boots BCs + Infra + Env for Project E
│   │   ├── customer-F
│   │   │   ├── env.ts
│   │   │   └── index.ts
│   │   └── dev-shell                       // Development environment treated as a project; includes tooling UIs
│   │       ├── infrastructure
│   │       │   ├── reload.ts               // Hot-reload logic
│   │       │   ├── time-mock.ts            // Mocked time for testing
│   │       │   └── watch.ts                // File watcher configuration
│   │       ├── delivery                    // overview.eta, dev-ws.js
│   │       │   └── overview.eta            // Dev status dashboard template
│   │       ├── env.ts                      // Dev-only configuration
│   │       └── index.ts                    // Wiring: Boots target project + Dev tools
│   ├── infrastructure                      // Implementation details; depends on Core and external libraries (SQLite, AWS S3, FS, etc.)
│   │   ├── shared                          // GENERIC TECHNICAL CAPABILITIES
│   │   │   ├── time                        // Cross-cutting concern
│   │   │   │   └── time-real.ts            // System clock implementation
│   │   │   ├── log                         // Cross-cutting concern
│   │   │   │   └── log-winston.ts          // Logger implementation
│   │   │   ├── inbound                     // INBOUND Transports (The "Entry")
│   │   │   │   ├── http                    // Express/Fastify setup, global middlewares
│   │   │   │   ├── cli                     // Command-line argument parsing logic
│   │   │   │   ├── websocket               // Socket.io/ws server setup
│   │   │   │   └── tcp                     // Raw net.Server logic
│   │   │   ├── outbound                    // OUTBOUND Transports (The "Exit")
│   │   │   │   ├── persistence             // Data storage adapters
│   │   │   │   │   ├── sqlite              // SQLite adapter
│   │   │   │   │   ├── mongodb             // MongoDB adapter
│   │   │   │   │   └── s3                  // S3 adapter
│   │   │   │   └── gateways                // External service clients
│   │   │   │       ├── smtp                // NodeMailer / SES wrappers
│   │   │   │       ├── sms                 // Twilio / MessageBird wrappers
│   │   │   │       └── push                // Firebase / Apple Push logic
│   │   │   ├── serialization               // TRANSFORMATION (The "Translators")
│   │   │   │   ├── templates               // Template engine setup
│   │   │   │   │   └── parent.eta
│   │   │   │   ├── json                    // Custom parsers (handling Dates, BigInt)
│   │   │   │   ├── protobuf                // Proto definitions and encoders
│   │   │   │   ├── xml                     // XML formatters
│   │   │   │   ├── html                    // HTML sanitizers/formatters
│   │   │   │   └── formatters              // Technical formatting (e.g., Byte-to-MB)
│   │   │   └── security                    // Auth, JWT, Encryption drivers
│   │   ├── bounded-context-A               // Domain A-specific infrastructure (Application Services)
│   │   │   ├── app.ts                      // Entry point for this context's services
│   │   │   ├── persistence                 // Repository implementations (raw SQL, ORM, etc.)
│   │   │   │   └── customer-repository.ts  // Implements core interface
│   │   │   ├── queries                     // Query implementations (raw SQL, ORM, etc.)
│   │   │   │   └── customer-queries.ts
│   │   │   ├── commands                    // Write orchestration
│   │   │   │   └── customer-evaluation.ts
│   │   │   └── scripts                     // Domain A-specific maintenance scripts
│   │   ├── bounded-context-B               // Domain B-specific infrastructure
│   │   └── runner.ts                       // The app orchestrator
│   └── core                                // Pure, side-effect-free business logic; depends on nothing (except minor utils)
│       ├── shared-kernel                   // Shared domain (Business logic used by multiple BCs; use sparingly)
│       │   ├── types.ts                    // Shared Value Objects (e.g., Currency, Email)
│       │   └── constants.ts
│       ├── bounded-context-A               // Pure domain logic (Context A)
│       │   ├── events                      // e.g. CustomerSignedUp, OrderPlaced
│       │   ├── queries                     // Read models
│       │   └── domain                      // Write models (entities, value objects)
│       │       └── customer.ts
│       ├── bounded-context-B               // Pure domain logic (Context B)
│       │   └── ...
│       ├── utils.ts                        // Generic TS helpers
│       └── utils.test.ts
├── static
│   ├── dev                                 // Browser files for development (e.g., dev-ws.js)
│   └── main                                // Production assets (Images, compiled CSS/JS)
│       └── style.css
├── test-integration                        // Cross-context tests (End-to-End flows)
│   ├── project-e.test.ts                   // Tests the fully wired Project E
│   └── xyz.test.ts
├── tmp                                     // Local dev files (VCS ignored)
│   ├── dev.state.db                        // Local sandbox DBs
│   └── ...
├── eslint.config.mjs
├── package.json
├── prettier.config.cjs
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock

Feature Example

To visualize how this structure works in practice, let's trace a standard feature: "A User signs up via a REST API."

Notice how the data flows from the "dirty" outside world into the "pure" core and back out, without the Core ever knowing about the HTTP server or the Database.

  1. The Entry (Inbound Infrastructure): The request hits src/infrastructure/inbound/http/controllers/auth.ts. This layer handles validation (JSON parsing) and converts the HTTP request into a DTO.
  2. The Orchestration (Context Infrastructure): The controller calls src/infrastructure/bounded-context-A/app.ts. This service orchestrates the flow. It does not contain business rules; it only coordinates.
  3. The Logic (Core Domain): The orchestrator instantiates a User entity from src/core/bounded-context-A/domain/user.ts. The entity enforces rules (e.g., "Password must be complex"). This file has zero dependencies.
  4. The Persistence (Outbound Infrastructure): The orchestrator passes the valid entity to src/infrastructure/bounded-context-A/persistence/user-repository.ts. This repository converts the pure Entity into a Database Record and saves it using the db/ schema definitions.
  5. The Response: The stack unwinds. The Controller returns a 201 Created.

The "Split Context" Strategy (Purity vs. Cohesion)

This layout strictly separates pure from side-effectful code. It means that the src/core has no dependencies to time, filesystem, network or any other ambient context whereas src/infrastructure does:

src/core/bounded-context-A (Pure domain logic)
src/infrastructure/bounded-context-A (Side-effectful rest)

The Trade-off:

This separation guarantees that business logic remains independent of frameworks and databases. However, it introduces a "Package by Layer" friction: adding a single feature (e.g., "Update Customer Email") requires navigating between src/core/... (to add the business rule) and src/infrastructure/... (to persist the change).

Alternative: For teams prioritizing velocity over strict architectural purity, consider Vertical Slices, where Core and Infrastructure sit side-by-side within the same module folder.

How to Collapse the Hierarchy (Scaling Down)

Almost no project needs this level of complexity. Use the following rules to collapse the structure for smaller scopes:

1. Collapse Environments (Single Tenant/Product)

  • Condition: You are building a single product with one deployment target.
  • Action: Delete src/environments. Move the contents of src/environments/project-E/* directly into src/app (or the root of src), effectively making the application its own single environment.

2. Collapse "Core vs. Infra" (Vertical Slices)

  • Condition: The application is CRUD-heavy, logic is simple, or rapid iteration is required.
  • Action: Merge domain and infrastructure inside the module.
    • Example: Create src/modules/users/service.ts containing both logic and database calls.
    • Note: This sacrifices strict architectural boundaries but significantly reduces file-hopping for simple features.

3. Collapse Transports (HTTP Only)

  • Condition: The application only serves HTTP requests (no WebSockets or CLI).
  • Action: Delete src/infrastructure/inbound. Configure your web framework (Express/Fastify) directly in src/index.ts or src/app.ts.

4. Collapse Database Layout (Code-First ORM)

  • Condition: You are using an ORM that synchronizes code entities directly to the database schema (e.g., TypeORM with synchronize: true).
  • Action: The migration subfolders within db/ become irrelevant. However, the db folder may still be retained for seed data or local database state files. Note: Relying on ORM synchronization is generally discouraged for production environments.

If you found this layout useful, please spread the word! 🚀
Sharing helps other developers find better ways to structure their code. I’m also eager to hear how you’ve adapted or "collapsed" this hierarchy for your own projects—feel free to reach out!

Tweet LinkedIn

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment