Lexicon
⌘K
Explore
ExploreGuidesNotes
WriteMy LibraryBooks
Sign in
Browse

Learning

  • Building a Sustainable Learning System

Networking

  • OSI Model
  • TCP vs UDP
  • TCP/IP Model

Programming

  • Start Here — How to Solve a Problem
  • From if/else to DSA — The Whole Path
  • Problem Solving
  • DSA Practice — Learn by Solving
  • C Programming
  • Python

Software Craft

  • How to Structure a Software Project
  • Debugging Software Systematically
  • Designing Clean and Maintainable User Interfaces

Tools & Workflow

  • A Practical Introduction to Git and Version Control
  • Introduction to Linux for Developers

Web & APIs

  • Understanding REST APIs

Writing

  • Writing Better Technical Documentation
New noteSuggest a guide

Lexicon

a personal knowledge base

Software Craft

How to Structure a Software Project

Updated 2026-07-09 · 7 min read

Project structure is one of those things nobody teaches you and everybody has opinions about. The truth is there is no single correct layout — but there are layouts that make a codebase easy to navigate and layouts that make every change feel like defusing a bomb.

The goal is simple: when you or a teammate needs to change something, it should be obvious where that thing lives. Good structure is mostly about making the answer to "where does this go?" boring and predictable.

Start simple, grow deliberately

The most common mistake in a new project is building the folder structure of a system you do not have yet. You create services/, repositories/, adapters/, and interfaces/ before you have written a single feature, and now you are maintaining scaffolding for imaginary complexity.

Start flat. A handful of files in a src/ folder is a perfectly good structure for a small project. Add folders when a folder would genuinely help you find things — usually when a directory grows past a dozen files or when a clear grouping emerges on its own.

Structure should be discovered, not imposed. Let the shape of the code tell you where the seams are, then formalize them. Refactoring a folder layout later is cheap; untangling premature abstraction is not.

Organize by feature, not by type

Here is the decision that matters most. There are two ways to group files:

By type (also called "by layer"):

src/
  controllers/
    userController.js
    orderController.js
  models/
    user.js
    order.js
  views/
    userView.js
    orderView.js

By feature (also called "by domain"):

src/
  users/
    userController.js
    user.js
    userView.js
  orders/
    orderController.js
    order.js
    orderView.js

For anything beyond a toy project, organize by feature. The reason is that changes are almost always feature-shaped, not type-shaped. When you add a field to orders, you touch the order controller, the order model, and the order view. If those files live together, the change is local and easy to reason about. If they are scattered across three type folders, you are jumping around the tree for one logical change.

Grouping by type feels tidy at first — all the models in one place! — but it optimizes for a question nobody asks ("show me every model") over the question everybody asks ("where is the order code?"). Feature folders keep related things together and make it obvious what can be deleted when a feature goes away.

Separate concerns with clear boundaries

Within a feature, still keep different responsibilities apart. The rough division that holds up across most applications:

  • Presentation — UI, components, templates. What the user sees.
  • Business logic — the rules that make your app yours. What actually happens.
  • Data access — talking to the database, files, or external APIs.

The key rule: dependencies should point inward. Your business logic should not import your web framework. It should not know whether it was called from an HTTP handler, a CLI command, or a test. That way you can test the logic without spinning up a server, and swap the database without rewriting the rules.

A practical smell test: if changing your web framework or database means editing your core business rules, the layers are tangled. A well-structured module has its important logic in plain functions that take data and return data, with the framework glue kept thin and at the edges.

Keep configuration and secrets out of the code

Configuration that changes between environments — database URLs, API keys, feature flags — does not belong hardcoded in source. The widely followed rule (from the Twelve-Factor App) is to keep config in the environment.

# .env — never commit this file
DATABASE_URL=postgres://localhost/myapp
STRIPE_KEY=sk_live_...
LOG_LEVEL=info

Then load it once at startup and pass it down. A few rules that save real pain:

  • Commit a .env.example with the keys and dummy values so newcomers know what is needed.
  • Add the real .env to .gitignore. Committing a secret means rotating it — treat any committed key as compromised.
  • Validate config at startup and fail loudly if something required is missing. A clear "DATABASE_URL is not set" at boot beats a mysterious crash under load.

Secrets in git history are one of the most common security incidents there is. See Git & Version Control for keeping them out in the first place.

Name things so the layout explains itself

Names are structure too. A good name lets someone predict a file's contents without opening it.

  • Be consistent with case. Pick kebab-case, camelCase, or snake_case for files and stick to it across the whole project.
  • Let names describe purpose, not type. auth.js tells me more than utils2.js.
  • Beware the utils or helpers dumping ground. One shared-helpers file is fine; when it hits a few hundred lines of unrelated functions, it has become a junk drawer. Split it by topic — dates.js, currency.js.
  • Match the file name to its main export. If a file's job is the OrderService, name it orderService.js, not service.js.
  • Keep a predictable place for tests. Either foo.test.js next to foo.js, or a mirrored tests/ tree — just be consistent.

A layout that scales

Putting it together, here is a structure that works for a medium-sized application and grows gracefully:

myapp/
  src/
    features/
      users/
      orders/
      billing/
    shared/          # genuinely cross-cutting code
      db/
      config/
      logging/
    app.js           # wires everything together
  tests/
  scripts/           # one-off and dev tooling
  docs/
  .env.example
  package.json
  README.md

The shared/ folder is where things go when they are truly used across features — but be strict about what earns a place there. Every item in shared/ is a dependency for the whole app, so a bloated shared folder quietly couples everything together. When in doubt, keep code inside the feature that uses it and only promote it to shared on the second or third real consumer.

The README.md at the root is not optional. It should say what the project is, how to run it, and how to run the tests — the three things every newcomer needs in their first ten minutes.

Common mistakes

  • Premature structure. Twelve folders and abstractions for a project with three features. Start flat, split when it hurts.
  • Organizing by type at scale. Fine for a tutorial, painful for a real app where changes are feature-shaped.
  • A bloated shared/ or utils/. Becomes a magnet for unrelated code and couples everything to everything.
  • Deep nesting. src/modules/features/user/components/profile/edit/index.js is a maze. If you are five folders deep, reconsider.
  • Circular dependencies. Feature A imports B which imports A. Usually a sign the boundary is in the wrong place; extract the shared piece.
  • Config in code. Environment-specific values baked into source make deploys fragile and leak secrets.
  • No obvious entry point. A newcomer should be able to find where the app starts in seconds.

Checklist

  • The layout is as flat as it can be while still grouping clearly.
  • Related code is grouped by feature, not scattered by type.
  • Business logic does not depend on the framework or database.
  • Configuration lives in the environment, with a committed .env.example.
  • The real .env and any secrets are gitignored.
  • Naming is consistent and files describe their purpose.
  • There is one obvious entry point and a README explaining how to run things.
  • shared/ contains only genuinely cross-cutting code.

Keep learning

Read the Twelve-Factor App once — it is short and its advice on config, dependencies, and processes underpins most modern deployment thinking. For the deeper "dependencies point inward" idea, look into hexagonal (ports and adapters) architecture; you do not need to adopt it wholesale to benefit from the mindset.

Then go read the source of a well-regarded open-source project in your language and notice how they lay things out — real codebases teach more than any diagram. Pair this with Writing Better Technical Documentation so your structure is explained, and Debugging Software Systematically — a clean structure makes bugs far easier to isolate.

← PreviousPythonNext →Debugging Software Systematically
Reading progress0%

Curated guide

Part of the Lexicon knowledge base — hand-written and kept up to date.

Browse all guides →