SaaS Folder Structure Best Practices

The essential playbook for implementing saas folder structure best practices in your SaaS.

A good SaaS folder structure reduces import chaos, keeps features isolated, and makes deployment, testing, and onboarding easier. For indie developers and small teams, the goal is not a perfect enterprise layout. The goal is a structure that supports fast iteration now and clean scaling later.

Quick Fix / Quick Setup

Use this as a practical default for a Flask or FastAPI SaaS app:

text
project/
├── app/
│   ├── api/
│   │   ├── dependencies/
│   │   ├── middleware/
│   │   └── routes/
│   ├── core/
│   │   ├── config.py
│   │   ├── security.py
│   │   └── logging.py
│   ├── db/
│   │   ├── models/
│   │   ├── migrations/
│   │   └── session.py
│   ├── services/
│   ├── repositories/
│   ├── schemas/
│   ├── tasks/
│   ├── templates/
│   ├── static/
│   └── main.py
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── scripts/
├── docs/
├── .env.example
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
└── README.md

Use this structure as the default baseline. Keep framework boot files small, move business logic into services/, and switch to domain-based modules once the app grows past a few routes.

Minimal backend boot file example:

python
# app/main.py
from fastapi import FastAPI
from app.api.routes import health, users, billing

app = FastAPI()

app.include_router(health.router)
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(billing.router, prefix="/billing", tags=["billing"])

Recommended config entrypoint:

python
# app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    app_name: str = "my-saas"
    environment: str = "development"
    database_url: str
    secret_key: str
    stripe_secret_key: str | None = None

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

settings = Settings()

What’s happening

Most SaaS codebases start flat: routes, models, templates, utilities, and config files all mixed together.

That works for an MVP, but it breaks down when auth, billing, background jobs, admin tools, and integrations are added.

A strong folder structure separates:

  • transport layer
  • business logic
  • data access
  • config
  • operational code

Main risks:

  • overengineering too early
  • adding too many layers before real complexity exists
  • letting route handlers become the place where everything happens

A good structure should make it obvious:

  • where new code belongs
  • where config is loaded
  • where database access happens
  • where background jobs live
  • how to test a feature without starting the whole app
Aspecta flat MVP structurea layered production-ready structure

side-by-side comparison of a flat MVP structure versus a layered production-ready structure.

Step-by-step implementation

1. Create one application root

Put runtime app code under a single app/ directory.

Why:

  • keeps imports predictable
  • avoids root-level file sprawl
  • makes packaging and test discovery easier

Example:

text
project/
├── app/
├── tests/
├── scripts/
└── README.md

2. Centralize core infrastructure

Put shared cross-cutting code in app/core/.

Use it for:

  • config
  • logging
  • security helpers
  • startup settings
  • feature flags
  • constants that are truly global

Example:

text
app/core/
├── config.py
├── logging.py
└── security.py

Do not scatter config loading across route files or service modules.

Related setup: Environment Variables and Secrets Management

3. Separate HTTP transport from business logic

Put endpoint handlers in app/api/routes/.

These files should mostly do:

  • request parsing
  • auth/session dependency use
  • schema validation
  • calling services
  • returning responses

They should not contain:

  • complex billing logic
  • large DB transaction flows
  • email sending logic
  • retry logic
  • multi-step business state changes

Example:

python
# app/api/routes/users.py
from fastapi import APIRouter, Depends
from app.schemas.user import CreateUserRequest, UserResponse
from app.services.user_service import create_user

router = APIRouter()

@router.post("/", response_model=UserResponse)
def create_user_endpoint(payload: CreateUserRequest):
    user = create_user(payload)
    return user

4. Add request-scoped dependencies and middleware

Use app/api/dependencies/ for:

  • current user resolution
  • DB session injection
  • tenant resolution
  • permission helpers

Use app/api/middleware/ for:

  • request IDs
  • CORS
  • structured logging hooks
  • rate limiting wrappers
  • audit headers

Example:

text
app/api/
├── dependencies/
│   ├── auth.py
│   └── db.py
├── middleware/
│   └── request_id.py
└── routes/

5. Isolate database concerns

Keep DB code under app/db/.

Suggested layout:

text
app/db/
├── models/
├── migrations/
└── session.py

Use session.py for engine/session creation:

python
# app/db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings

engine = create_engine(settings.database_url, pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)

Keep ORM models out of route files.

Keep migration history in one dedicated place and apply it consistently in development, staging, and production.

Related setup: Development vs Production Environments

6. Use schemas for API contracts

Put request/response validation objects in app/schemas/.

Examples:

  • Pydantic models
  • Marshmallow schemas
  • serializer objects

Example:

python
# app/schemas/user.py
from pydantic import BaseModel, EmailStr

class CreateUserRequest(BaseModel):
    email: EmailStr
    password: str

class UserResponse(BaseModel):
    id: str
    email: EmailStr

Benefits:

  • cleaner route handlers
  • explicit contracts
  • easier test assertions
  • safer refactors

7. Move business logic into services

Use app/services/ for workflows such as:

  • signup flow
  • subscription activation
  • invoice generation
  • email verification
  • report generation
  • webhook processing

Example:

python
# app/services/user_service.py
from app.db.models.user import User

def create_user(payload):
    user = User(email=payload.email)
    # hashing, validation, persistence, event trigger
    return user

If a service grows too large, split by domain:

text
app/services/
├── auth_service.py
├── billing_service.py
└── user_service.py

or later:

text
app/modules/
├── auth/
├── billing/
└── users/

8. Add repositories only when needed

A repositories/ layer is optional.

Use it when:

  • ORM queries are duplicated across services
  • query logic becomes hard to test
  • you want consistent access patterns for reads/writes

Skip it if the app is still small.

Bad pattern:

  • adding repository abstractions before there is enough complexity to justify them

9. Create a dedicated tasks layer

Put async or scheduled jobs in app/tasks/.

Use it for:

  • sending emails
  • webhook retries
  • scheduled cleanup
  • report generation
  • background sync jobs

Example:

text
app/tasks/
├── emails.py
├── billing_retries.py
└── cleanup.py

If you use Celery, RQ, or FastAPI background tasks, keep job code here rather than mixing it into request handlers.

10. Keep frontend assets separate from backend logic

Only use templates/ and static/ if the backend serves HTML or assets directly.

text
app/
├── templates/
└── static/

If you have a separate frontend, keep frontend code out of the Python app tree or use a clearly separate workspace directory.

11. Mirror the app structure in tests

Use:

text
tests/
├── unit/
├── integration/
└── e2e/

Guidelines:

  • unit/: service functions, pure helpers, validation
  • integration/: DB, queues, external adapters, API slices
  • e2e/: full user flows

Example:

text
tests/
├── unit/test_user_service.py
├── integration/test_signup_api.py
└── e2e/test_checkout_flow.py

Useful command:

bash
pytest --collect-only

12. Put operational scripts outside runtime code

Create a scripts/ directory for:

  • data backfills
  • admin actions
  • seeding
  • maintenance jobs
  • repair scripts
  • migration helpers

Example:

text
scripts/
├── seed_dev_data.py
├── backfill_customer_ids.py
└── reprocess_failed_webhooks.py

Do not leave these files at the repo root.

13. Standardize naming before the app gets bigger

Choose conventions early:

  • singular vs plural directories
  • import style
  • module naming
  • test naming
  • service naming
  • migration workflow

Example rules:

  • models/, schemas/, routes/
  • one config source: app/core/config.py
  • one app entrypoint: app/main.py
  • no wildcard imports
  • no generic utils.py dumping ground

Useful check:

bash
grep -R "import \*" app

Common causes

Folder structures usually become messy for a few repeatable reasons:

  • starting with a flat structure and never refactoring as features increase
  • putting business logic directly inside Flask or FastAPI route handlers
  • mixing app code, deployment files, migration files, and admin scripts without boundaries
  • using generic files like utils.py or helpers.py as catch-all storage
  • creating too many layers too early, which slows development and makes navigation harder
  • not defining a consistent place for config, database access, and background jobs
  • letting tests drift into random locations so coverage becomes hard to maintain

Debugging tips

Use these commands to inspect structure drift and misplaced logic.

Inspect the repository tree

bash
find . -maxdepth 3 -type f | sort
bash
tree -L 3
bash
find app -type f | sort

Find scattered config loading

bash
grep -R "os.getenv\|dotenv\|BaseSettings" app

If many files load env vars directly, config is too distributed.

Find route definitions

bash
grep -R "@app\.|APIRouter\|Blueprint" app

If route files are huge or mixed with unrelated logic, split transport from services.

Find database session creation

bash
grep -R "create_engine\|Session\|sessionmaker" app

If DB setup appears in multiple files, centralize it in app/db/session.py.

Find background task code

bash
grep -R "Celery\|rq\|Redis\|BackgroundTasks" app

If tasks are scattered across routes and services, move them into app/tasks/.

Find unsafe imports

bash
grep -R "import \*" app

Wildcard imports make module boundaries harder to trace.

Validate test discovery

bash
pytest --collect-only

If tests are not discoverable, naming or folder structure is inconsistent.

Catch import and syntax issues

bash
python -m compileall app

Checklist

  • There is one clear application root directory.
  • Config, security, and logging are centralized.
  • Business logic is outside route handlers.
  • Database models and migrations are separated from request code.
  • Background jobs live in a dedicated folder.
  • Tests mirror application structure.
  • Operational scripts are stored outside runtime app code.
  • Secrets are not committed; .env.example exists.
  • Root directory is not cluttered with ad hoc scripts and backup files.

Use this with: SaaS Production Checklist

Related guides

FAQ

What is the best folder structure for a small SaaS?

Use a simple layered structure with app/, core/, api/, db/, services/, tasks/, and tests/. Add feature modules later when the codebase grows.

Should I split code by feature from day one?

Usually no. For an MVP, a clean layered structure is faster. Split by feature once auth, billing, notifications, and teams become distinct domains.

Where should environment config live?

Put it in a single config module such as app/core/config.py and load values from environment variables, not scattered constants.

Where should deployment files go?

Keep Dockerfile, compose files, CI config, and infrastructure files at the repo root or in a dedicated deploy/ or infra/ folder, separate from runtime app code.

How do I know my current structure is failing?

If new features require editing many unrelated files, imports are hard to trace, route files are very large, or duplicate logic appears across modules, the structure needs cleanup.

Final takeaway

The best folder structure is the one that keeps code discoverable, reduces coupling, and supports safe iteration.

For most small SaaS apps, start with:

  • app/
  • core/
  • api/
  • db/
  • services/
  • tasks/
  • tests/

Avoid both extremes:

  • one giant flat folder
  • enterprise-grade architecture before you need it

If your current project is still small, use a layered structure now. Move to domain-based modules only when real feature boundaries appear.