Project Structure &
Directory Architecture
A practical reference for translating system architecture into folder structure — with real examples across different project types. Use this as a planning tool when starting something new.
Core Rules
Before looking at any specific project type, internalize these rules. They apply regardless of language, stack, or scale.
Architecture → Directory Translation
Name by purpose, not type
Build what you need now
Don't create folders for things that don't exist yet. A folder with one file in it is a sign you over-structured. Start flat, split when a file gets too large or a second distinct concern appears.
Data flow = folder order
Read your folder list top to bottom — it should roughly match how data flows through your system. If it doesn't, your structure may not match your architecture.
config/ and main are universal
Almost every project needs a place for configuration and an entry point that wires everything together and stays thin. If
Planning Process
The fastest way to get a good directory structure is to spend five minutes answering questions before creating any files. Architecture first, folders second.
- What are the major components of this system? Draw them as boxes.
- What does each component own and what does it do?
- How does data flow between components? Draw the arrows.
- What could be swapped out independently? (database, API layer, frontend framework)
- What external services or libraries does this depend on?
- What needs to be kept secret? (API keys, credentials → .env)
- Will this run in multiple environments? (dev, staging, prod → config/)
- What is the entry point — where does execution begin?
Once you've answered these, your boxes become folders, your answers about what each box does become files, and your arrows become imports. The structure should feel obvious rather than designed.
Step 1 — Sketch the architecture
Even a rough list of components on paper or in a markdown file is enough. You're not designing a system — you're identifying the nouns. "I need something that fetches data, something that processes it, something that stores it, and something that serves it."
Step 2 — Map to folders
Each noun from step 1 becomes a folder. Then ask what the distinct jobs are inside each noun — those become files. Don't go deeper than two levels until you have a reason to.
REST API
A REST API receives HTTP requests, processes them, talks to a database, and returns responses. The architecture is almost always: router → handler → service/business logic → database. Each layer maps directly to a folder.
my-api/ ├── cmd/ │ └── main.go # entry point — wires everything, starts server │ ├── internal/ # Go convention: not importable by outside packages │ ├── handlers/ │ │ ├── user_handler.go # HTTP handlers for /users routes │ │ └── post_handler.go # HTTP handlers for /posts routes │ ├── middleware/ │ │ ├── auth.go # JWT validation, session checks │ │ └── logging.go # request logging │ ├── models/ │ │ ├── user.go # User struct, validation │ │ └── post.go # Post struct, validation │ └── service/ │ ├── user_service.go # business logic for users │ └── post_service.go # business logic for posts │ ├── db/ │ ├── queries.go # SQL queries │ └── migrations/ # versioned schema changes │ ├── 001_create_users.sql │ └── 002_create_posts.sql │ ├── config/ │ └── config.go # reads .env, exposes typed config struct │ ├── .env # DB_URL, JWT_SECRET, PORT — never committed ├── .gitignore └── go.mod
- What resources does this API expose? Each resource likely gets its own handler file.
- Is there business logic that sits between the handler and the database? That's your service layer.
- What needs authentication? That's middleware, not handler logic.
- Will the database schema change over time? You need migrations from day one.
- What configuration changes between environments? That goes in config/, not hardcoded.
cmd/ vs internal/
Go convention:
Handler vs Service
Handlers deal with HTTP — reading request params, writing responses, status codes. Services deal with business logic — validation, orchestration, rules. If your handler is doing business logic, it's in the wrong layer.
Full Stack Web App
A full stack app has two distinct codebases living in one repository — a frontend that runs in the browser and a backend that runs on a server. The top-level split is always client vs server. Everything else follows from that.
my-app/ │ ├── client/ # everything that runs in the browser │ ├── src/ │ │ ├── components/ # reusable Vue components │ │ ├── views/ # page-level components (one per route) │ │ ├── stores/ # Pinia state management │ │ ├── api/ # functions that call the Go backend │ │ └── types/ # shared TypeScript interfaces │ ├── vite.config.ts │ ├── tsconfig.json │ └── package.json │ ├── server/ # everything that runs on the server │ ├── cmd/ │ │ └── main.go │ ├── internal/ │ │ ├── handlers/ │ │ ├── middleware/ │ │ ├── models/ │ │ └── service/ │ ├── db/ │ │ └── migrations/ │ └── go.mod │ ├── config/ │ └── .env # shared secrets: DB_URL, API keys │ └── docker-compose.yml # optional: spin up DB and services together
- Are client and server truly separate? If the frontend is server-rendered, the structure changes significantly.
- How does the frontend call the backend? Define that API contract early — it's the seam between the two halves.
- What state lives on the client vs the server? Client state goes in stores/, server state goes in the DB.
- Will you run these as separate services or together? Separate → each gets its own repo eventually.
- What's shared between client and server? Type definitions, validation rules — consider a shared/ folder.
Graphics Application
The defining rule of graphics apps: simulation and rendering are always separate. The code that runs physics, manages entities, and tracks state must never know how it gets drawn. This separation lets you swap rendering backends without touching simulation logic — and it keeps your mental model clean.
particle-sim/ │ ├── src/ │ ├── main.cpp # entry point — loop only, no logic │ │ │ ├── simulation/ # knows nothing about rendering │ │ ├── emitter.h │ │ ├── emitter.cpp # spawns particles, manages lifetime │ │ ├── physics.h │ │ └── physics.cpp # gravity, velocity, forces │ │ │ └── renderer/ # knows nothing about simulation logic │ ├── renderer.h │ └── renderer.cpp # owns window, draws what it's given │ ├── assets/ # textures, fonts, sounds │ ├── build/ # generated by CMake — never commit this │ ├── CMakeLists.txt # describes how to compile the project ├── vcpkg.json # declares C++ dependencies (SFML, etc.) └── .gitignore # must include build/ and .env
- What are the entities in your simulation? Each distinct entity type may warrant its own file.
- What does the renderer need to know? Only what to draw — position, color, shape. Nothing else.
- What is the game loop doing each frame? Poll events → update → clear → draw → display. Map each step to a method.
- Are there multiple systems? (physics, AI, audio) Each is a candidate for its own folder inside simulation/.
- What libraries do you need? List them in vcpkg.json before writing a line of code.
The loop shape is universal
Poll events → update simulation → clear → draw → display. This loop appears in SFML, OpenGL, Vulkan, Unity, and Unreal. Only the complexity inside each step changes. Get comfortable with the shape here.
Delta time, always
Tie all simulation updates to real elapsed time, not frame count.
Discord Bot
Discord bots are event-driven — they react to things happening (a message sent, a user joining, a slash command used). The architecture maps directly to that: commands are one folder, events are another, and everything that supports them lives underneath.
my-bot/ │ ├── index.ts # entry point — registers events, logs in │ ├── src/ │ ├── commands/ # one file per slash command │ │ ├── ping.ts │ │ ├── ban.ts │ │ └── stats.ts │ │ │ ├── events/ # one file per Discord event type │ │ ├── ready.ts # fires when bot connects │ │ ├── messageCreate.ts # fires on every message │ │ └── guildMemberAdd.ts # fires when someone joins │ │ │ ├── db/ # if the bot stores data │ │ ├── client.ts # DB connection │ │ └── queries.ts # read/write operations │ │ │ └── lib/ # shared utilities │ ├── embed_builder.ts # reusable Discord embed templates │ └── permissions.ts # permission check helpers │ ├── config/ │ └── .env # DISCORD_TOKEN, CLIENT_ID, DB_URL │ ├── tsconfig.json └── package.json
- What commands does the bot need? Each one becomes a file in commands/.
- What Discord events does the bot respond to? Each one becomes a file in events/.
- Does the bot need to remember anything between sessions? If yes, you need db/.
- Are there things multiple commands share? (formatting, permission checks) That goes in lib/.
- Will commands be loaded dynamically or registered manually? Dynamic loading means a command loader utility in lib/.
Data Pipeline / ETL
ETL stands for Extract, Transform, Load — and the folder structure maps to it almost literally. Data comes in from somewhere, gets cleaned and reshaped, and lands somewhere else. The architecture is a pipeline, and the directory reflects that linearity.
my-pipeline/ │ ├── main.py # entry point — runs the pipeline │ ├── extract/ # pull raw data from sources │ ├── api_client.py # fetch from external APIs │ ├── file_reader.py # read CSV, JSON, XML │ └── db_reader.py # query source database │ ├── transform/ # clean, reshape, enrich │ ├── cleaner.py # remove nulls, fix types, deduplicate │ ├── normalizer.py # standardize formats (dates, currency, etc.) │ └── enricher.py # join with lookup data, derive new fields │ ├── load/ # write processed data to destination │ ├── db_writer.py # insert/upsert to database │ └── file_writer.py # write output files │ ├── scheduler/ # if the pipeline runs on a schedule │ └── cron.py │ ├── data/ │ ├── raw/ # untouched source data — never modify │ └── processed/ # output of the pipeline │ ├── config/ │ └── .env # API_KEY, DB_URL, OUTPUT_PATH │ └── requirements.txt
- Where does the data come from? Each distinct source may need its own extractor file.
- What transformations are needed? Separate cleaning (fixing bad data) from normalizing (reshaping good data).
- Where does the data go? Each destination gets its own loader.
- Does raw data need to be preserved? If yes, never let transform/ overwrite extract/ output — use data/raw/.
- Does this run once or on a schedule? If scheduled, you need a scheduler and idempotent load logic.
Game Project
Godot uses its own virtual filesystem (
my-game/ # this is res:// in Godot │ ├── scenes/ # scene files (.tscn), organized by type │ ├── world/ # levels, maps, environments │ ├── entities/ # player, enemies, NPCs │ └── ui/ # menus, HUD, dialogs │ ├── scripts/ # GDScript files, mirroring scenes/ structure │ ├── world/ │ ├── entities/ │ └── ui/ │ ├── assets/ │ ├── textures/ │ ├── audio/ │ │ ├── music/ │ │ └── sfx/ │ └── fonts/ │ ├── autoloads/ # Godot singletons — global state, event bus │ ├── game_manager.gd │ └── audio_manager.gd │ ├── addons/ # third-party Godot plugins │ ├── extension/ # C++ GDExtension code (if used) │ ├── src/ │ ├── CMakeLists.txt │ └── vcpkg.json │ └── project.godot # Godot project config — always at root
- What are the major entity types? (player, enemies, environment, UI) These become subfolders inside scenes/ and scripts/.
- What needs to be globally accessible? Autoloads (singletons) — but use them sparingly or they become a dumping ground.
- What state persists between scenes? Game manager autoload, save file system.
- Will you write any C++? If yes, set up the extension/ folder and CMake structure from the start — it's harder to retrofit.
- Are scripts co-located with scenes or separate? Either works, but pick one and stay consistent throughout.
res:// is your project root
Every path in Godot is relative to
Autoloads are global state
Godot autoloads (singletons) are accessible from anywhere in your game. Useful for things like a global event bus or audio manager — but easy to abuse. If too many things live in autoloads, you've created a hidden dependency web. Keep them focused and few.
Twitter / Social Graph Visualizer
This app pulls users and tweets from an API, classifies users by interest, builds a graph of relationships, stores it in a graph database, serves it through an API, and renders it as an interactive node visualization in the browser. It has more layers than most projects — each with a distinct job — and the data flow is strictly one-directional until it reaches the frontend.
twitter-viz/ │ ├── main.py # entry point — starts the pipeline and/or API server │ ├── ingestion/ # pulls raw data from the Twitter API │ ├── client.py # auth, session, API connection │ ├── fetcher.py # pulls tweets, users, follower graphs │ └── rate_limiter.py # handles API rate limits gracefully │ ├── processing/ # cleans data and builds the graph │ ├── cleaner.py # deduplicate, normalize, fix bad data │ ├── classifier.py # categorize users by topic/interest │ └── graph_builder.py # derive node/edge relationships │ ├── db/ # Neo4j graph database layer │ ├── client.py # Neo4j connection and session │ ├── queries.py # Cypher read/write operations │ └── schema.py # node labels, relationship types, constraints │ ├── api/ # FastAPI — serves data to the frontend │ ├── routes.py # /nodes, /edges, /users/:id endpoints │ ├── serializers.py # shape Neo4j results into JSON the frontend expects │ └── server.py # FastAPI app instantiation │ ├── frontend/ # browser visualization │ ├── src/ │ │ ├── graph.ts # D3 force-directed graph, node/edge rendering │ │ ├── filters.ts # UI controls — filter by topic, time, degree │ │ └── api.ts # fetch calls to the FastAPI backend │ └── index.html │ ├── config/ │ └── .env # TWITTER_API_KEY, NEO4J_URI, NEO4J_PASSWORD │ └── requirements.txt
- What is the unit of data? A user? A tweet? A topic cluster? That becomes your primary graph node.
- What relationships matter? Follows, retweets, shared interests — each becomes a graph edge type in Neo4j.
- How do you classify users by interest? Keyword matching, ML clustering, hashtag frequency — this is your classifier's job and the hardest part.
- Does the pipeline run once or continuously? Continuous ingestion needs a scheduler and upsert logic in db/.
- What does the frontend actually need from the API? Define the JSON shape early — it determines what serializers.py has to produce.
- How many nodes can D3 render before it gets slow? Plan for pagination or level-of-detail filtering in the API from the start.
Why Neo4j, not Postgres
You're modeling a graph — users connected to other users by relationships. In a relational DB, querying "who are the 3rd-degree connections of this user?" requires expensive joins. In Neo4j, that's a single Cypher query traversing edges. When your primary data structure is a graph, use a graph database.
ingestion never touches the DB directly
The ingestion layer's only job is to pull raw data and hand it off. Processing cleans and classifies it. Only then does it hit the database. This keeps each layer testable in isolation — you can run the classifier against a fixture file without needing a live Twitter API or a running Neo4j instance.
Live Audio Visualizer ✦
Captures audio from a microphone or file in real time, runs a Fast Fourier Transform (FFT) to convert the raw waveform into frequency data, and renders that data as a live animated visualization — frequency bars, waveforms, spectrograms. This project sits at the intersection of signal processing, real-time systems, and graphics, and it maps cleanly onto the simulation/renderer architecture you already know.
The key insight: audio analysis is the simulation layer. The FFT output is just data — a buffer of frequency magnitudes. The renderer reads that buffer and draws it. Neither layer knows anything about the other.
audio-viz/ │ ├── src/ │ ├── main.cpp # entry point — loop only │ │ │ ├── audio/ # capture raw audio samples │ │ ├── capture.h │ │ ├── capture.cpp # mic input via SFML SoundRecorder │ │ └── sample_buffer.h # thread-safe ring buffer of raw samples │ │ │ ├── analysis/ # signal processing — the "simulation" layer │ │ ├── fft.h │ │ ├── fft.cpp # wraps FFTW — converts samples → frequency bins │ │ ├── spectrum.h │ │ └── spectrum.cpp # smoothing, peak detection, band grouping │ │ │ ├── renderer/ # draws the frequency data — knows nothing about audio │ │ ├── renderer.h │ │ ├── renderer.cpp # owns SFML window, dispatches to draw modes │ │ ├── bar_visualizer.cpp # classic frequency bar chart │ │ ├── waveform.cpp # raw waveform oscilloscope view │ │ └── spectrogram.cpp # scrolling time-frequency heat map │ │ │ └── core/ # shared types used across layers │ └── frequency_data.h # the struct passed from analysis → renderer │ ├── assets/ │ └── fonts/ # for any on-screen labels │ ├── build/ # CMake output — never commit │ ├── CMakeLists.txt ├── vcpkg.json # sfml, fftw3 └── .gitignore
- What is the data contract between analysis and renderer? Define
FrequencyData first — it's the seam between the two halves of the app. - How do you handle the audio/render thread mismatch? Audio capture runs on its own thread; the render loop runs on the main thread. You need a thread-safe buffer between them.
- What FFT window size do you want? Larger = better frequency resolution, worse time resolution. Smaller = snappier response, less detail. This is a design decision, not a code decision.
- What visualization modes do you want? Each mode is a separate file in renderer/ — plan them upfront so the renderer can switch between them cleanly.
- Are you capturing live mic input or analyzing a file? Both are valid — but the capture layer changes significantly. Design for one first, then abstract.
core/ — the shared contract
Multiple visualizers, one renderer
Universal Files
These files appear in almost every project regardless of language or stack. Set them up at the start — retrofitting them is always messier than starting with them.
.env
Secrets and environment-specific config. API keys, database URLs, ports. Never committed to Git. Everyone on the team has their own local copy.
.gitignore
Tells Git what to exclude. Always include:
Dependency manifest
Go:
README.md
What the project is, how to run it, what environment variables are needed. Future you will thank present you. Even a few sentences is better than nothing.
config/
Code that reads from
Entry point
Common Mistakes
Too flat
Everything in root, one giant entry point. Works for a weekend project, impossible to navigate after a month. Split when a file exceeds one clear responsibility.
Too deep
Folders nested four levels down with one file each. Structure should reflect real complexity, not imagined future complexity. Flatten until it hurts, then split.
Naming by type
Crossing layer boundaries
The renderer importing from simulation logic. The database layer knowing about HTTP. Handlers containing business logic. Each layer should only talk to the one directly below it.
Committing secrets
A
Logic in main
The entry point grows into the largest file in the project. If