Vasu Patel logoVasu Patel
← Clean by Default: Coding Standards
All chapters in this track
  1. 01Chapter 1: Why Coding Standards Matter: Readability, Onboarding, Defects, Consistency at Scale
  2. 02Chapter 2: Naming Conventions
  3. 03Chapter 3: Formatting and Style
  4. 04Chapter 4: Comments and Self-Documenting Code
  5. 05Chapter 5: Error Handling Conventions
  6. 06Chapter 6: Code and Project Organization (Files, Modules, Packages)
  7. 07Chapter 7: Writing Testable Code
  8. 08Chapter 8: Commit Hygiene: Atomic Commits, Messages, PR-Sized Changes
  9. 09Chapter 9: Language-Specific Idioms and Anti-Patterns Across TypeScript and Python
  10. 10Chapter 10: Security-Conscious Basics: Input Validation, Secrets Handling, Injection Risks
  11. 11Chapter 11: Enforcing Standards Automatically: Linters, Formatters, CI Gates
  12. 12Chapter 12: Creating, Documenting, and Evolving a Team Style Guide (Exceptions and Legacy Code)
  13. 13Chapter 13: Worked Example: Refactoring a Messy Function to Standard
  14. 14Chapter 14: Worked Example: Drafting a Style Guide Section From Scratch
  15. 15Chapter 15: Common Interview Questions About Coding Standards and Code Quality

Chapter 6 of 15

Chapter 6: Code and Project Organization (Files, Modules, Packages)

Why this matters

A well-organized codebase lets an engineer predict where to find (or add) code without being told — a poorly organized one requires institutional memory, tribal knowledge, or a frustrating search every single time. Organization standards exist to make a codebase navigable by pattern recognition, the same payoff Chapter 1 identified for naming conventions, now applied at the scale of files and directories.

Organizing by feature vs. organizing by type

[BEGINNER] Two dominant strategies, each with real trade-offs:

Organizing by technical type (layer-first):

src/
  controllers/
    userController.ts
    orderController.ts
  services/
    userService.ts
    orderService.ts
  models/
    user.ts
    order.ts

Organizing by feature (feature-first):

src/
  users/
    userController.ts
    userService.ts
    user.model.ts
  orders/
    orderController.ts
    orderService.ts
    order.model.ts

[SENIOR INSIGHT] Feature-first organization scales better as a codebase grows, for a concrete, structural reason: working on "orders" functionality with layer-first organization requires jumping between three or more distant directories for every single change, while feature-first organization keeps everything related to one feature physically co-located. Layer-first organization also tends to make it harder to eventually extract a feature into its own service or package (a common evolution for growing systems), since its code is scattered across the entire codebase's directory structure rather than living in one place. Layer-first organization can feel natural for small codebases (where "jumping between three directories" is trivial because each directory only has a handful of files) — the trade-off shifts as a codebase grows, which is worth stating explicitly rather than treating one approach as universally correct.

Module boundaries: encapsulation applied to file/directory structure

[BEGINNER] Directly extending the OOP book's Chapter 3 encapsulation principle to a codebase's physical structure: a module (a directory, a package) should expose a clear public interface and hide its internal implementation details, the same way a well-encapsulated class hides its private fields.

// users/index.ts -- the module's PUBLIC interface, explicitly exported
export { createUser, getUserById, updateUser } from "./userService";
export type { User } from "./types";

// users/internalHelpers.ts -- NOT exported from index.ts -- an implementation
// detail other modules should never import directly, even though the language
// might technically allow it.
# users/__init__.py -- explicitly controls what's part of the module's public interface
from .service import create_user, get_user_by_id, update_user
from .types import User

__all__ = ["create_user", "get_user_by_id", "update_user", "User"]
# Anything NOT listed here (e.g., users/internal_helpers.py) is an implementation
# detail -- other modules importing it directly are reaching past the intended boundary.

[IN PRACTICE] This convention matters specifically because it makes refactoring safe and predictable: if a module's internal implementation details are never imported directly by other modules (enforced by convention, and ideally by linting — Chapter 11), that module's internals can be freely restructured without needing to hunt down every external caller — exactly the same "swap the implementation without breaking callers" payoff the OOP book's Chapter 4 (abstraction) describes, now applied at the module level instead of the class level.

File size and the "one clear purpose" heuristic

[SENIOR INSIGHT] A hard line-count limit on files (e.g., "no file over 300 lines") is a blunt, sometimes-wrong instrument — some genuinely cohesive files are long because the problem they solve is inherently that large, and splitting them arbitrarily to satisfy a line count can actually hurt readability by scattering related logic. A better heuristic: a file should have one clear, statable purpose — if you can't summarize what a file is responsible for in one sentence without using "and" to join unrelated responsibilities, it's a candidate for splitting, regardless of its actual line count. A 150-line file doing three unrelated things is a worse organizational unit than an 800-line file doing one large, cohesive thing well.

Import organization and avoiding circular dependencies

[BEGINNER] Consistent import ordering (standard library imports, then third-party, then local/internal — a common convention across many languages and enforced automatically by most linters) reduces visual noise and merge conflicts. More importantly: circular dependencies (module A imports from module B, which imports from module A) are a structural warning sign, not just a style violation — they indicate the two modules' responsibilities aren't actually cleanly separated, and often signal that shared logic should be extracted into a third module both can depend on instead.

// orders/orderService.ts imports from users/userService.ts
// users/userService.ts imports from orders/orderService.ts
// CIRCULAR -- this usually means "order" and "user" logic are more tangled
// than the module boundary suggests; extract the shared logic both need
// into a separate module that BOTH can depend on, breaking the cycle.

Common mistakes

  1. Organizing a growing codebase purely by technical layer, forcing every feature-level change to jump across many distant directories, and making it hard to eventually extract a feature into its own service.
  2. Not defining or enforcing a module's public interface, letting other modules reach directly into internal implementation files — this makes safe refactoring of that module's internals effectively impossible without a wide, unpredictable blast radius.
  3. Enforcing a rigid file line-count limit without considering cohesion, sometimes splitting a genuinely cohesive large file into artificially fragmented, harder-to-follow pieces just to satisfy a number.
  4. Allowing circular dependencies to accumulate without recognizing them as a structural signal that two modules' responsibilities need to be untangled, or that a shared dependency needs to be extracted.

Checklist: organizing code and projects well

  1. Default to feature-first organization for anything beyond a small codebase, keeping related code physically co-located rather than scattered across technical layers.
  2. Define and enforce each module's public interface explicitly (via index.ts/__init__.py exports, or your language's equivalent), and avoid importing directly from another module's internal files.
  3. Judge a file's organization by whether it has one clear, statable purpose, not by a rigid line-count limit — split for cohesion reasons, not just size.
  4. Treat a circular dependency as a design smell to resolve, not a technical inconvenience to route around — extract shared logic into a module both sides can depend on instead.

Discussion / interview questions

  1. "What's the trade-off between organizing code by technical layer versus by feature?" Strong answer: Layer-first groups similar technical concerns together but forces feature-level changes to jump across many directories, and complicates eventually extracting a feature into its own service; feature-first co-locates everything related to one feature, scaling better as a codebase grows, though it can feel like unnecessary structure for a very small codebase.

  2. "How does encapsulation apply to file and module organization, beyond just class design?" Strong answer: A module should expose an explicit public interface (via controlled exports) and hide its internal implementation files from direct external import — this lets the module's internals be freely refactored without needing to track down every external caller, the same payoff class-level encapsulation provides, applied at the module/directory level.

  3. "Would you enforce a strict maximum line count per file? Why or why not?" Strong answer: Not as a rigid rule — a better heuristic is whether a file has one clear, statable purpose; a large but genuinely cohesive file can be fine, while splitting it arbitrarily to satisfy a line count can scatter related logic and hurt readability instead of helping it.

  4. "You discover a circular dependency between two modules. What does this usually indicate, and how would you fix it?" Strong answer: Usually indicates the two modules' responsibilities aren't cleanly separated, or that they share some logic that should live in a common place; the standard fix is extracting the shared logic into a third module that both original modules can depend on, breaking the cycle.

Chapter summary / cheat sheet

  • Feature-first organization generally scales better than layer-first as a codebase grows, keeping related code physically co-located.
  • Apply encapsulation to modules, not just classes: define an explicit public interface (controlled exports) and treat internal files as private implementation details other modules shouldn't import directly.
  • Judge file organization by cohesion ("one clear, statable purpose"), not a rigid line-count limit.
  • Circular dependencies are a structural design smell — fix by extracting shared logic into a common dependency, not by routing around the cycle.