Module Architecture
Learn how to structure and scale large Python applications. Master the architectural patterns used by Django, FastAPI, Airflow, and enterprise teams to build maintainable codebases with thousands of files.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Module & Package Architecture for Large Codebases
When your project grows beyond a few files, the MOST important factor for long-term success is structure. A great architecture makes your code:
- ✔ easier to understand
- ✔ easier to test
- ✔ easier to extend
- ✔ easier to debug
- ✔ easier to onboard new developers
- ✔ scale to thousands of files
This lesson teaches you exactly how professional Python teams structure + scale large applications.
🔥 1. Why Python Projects Need Good Architecture
- ❌ circular imports
- ❌ duplicated logic
- ❌ unclear module responsibilities
- ❌ "god files" with 5,000–20,000 lines
- ❌ impossible navigation
- ❌ hard-to-test logic
- ❌ breaking one feature breaks everything
- modular isolation
- clear domain boundaries
- strong naming conventions
- proper folder hierarchy
- dependency direction
- package-level APIs
⚙️ 2. How Python Imports Actually Work
- It searches directories from sys.path
- Looks for a package folder OR .py file
- Executes the module once
- Caches it in sys.modules
- ✔ imports are cached
- ✔ circular imports cause runtime errors
- ✔ module execution on import can be expensive
👉 Keep import side-effects to zero. (No DB connections, no heavy computation.)
📦 3. What Is a Package? (And Why It Matters)
A package is a folder containing an __init__.py .
Without __init__.py , Python treats folders as namespace packages.
Use regular packages unless you need distributed namespace packages.
🧱 4. Standard Large-Scale Project Structure
Professional Python projects (Django, Flask, Airflow, FastAPI) use this format:
- domain layer contains business logic
- infrastructure contains external systems
- api layer exposes HTTP / CLI interface
- core holds shared primitives
- config holds settings
🧩 5. The Layered Architecture (Most Common Design)
- abstractions
- ✔ isolates business logic
- ✔ minimizes circular imports
- ✔ pluggable infrastructure (swap DB easily)
- ✔ easier unit testing
🔌 6. Dependency Direction (The #1 Rule)
High-level modules must NOT depend on low-level modules.
Instead → low-level depends on high-level interfaces.
- ✔ dependency injection
- ✔ testing with mock DB
- ✔ clean separation
- ✔ avoiding circular imports
🔍 7. Avoiding Circular Imports
Circular imports happen when two modules import each other:
- ✔ moving shared logic into core
- ✔ using local imports inside functions
- ✔ introducing interfaces
- ✔ separating pure logic from IO
🧠 8. Internal Package APIs (__all__ and public API)
- ✔ clean external imports
- ✔ hides internal details
- ✔ stable API for other modules
🚀 9. Configuration Architecture
- settings_local.py
- environment variables
Avoid hard-coding secrets, URLs, DB credentials.
📚 10. Module Naming Conventions (Professional Standard)
- models.py — classes representing data
- services.py — business logic
- repository.py — DB access
- routes.py — API routes
- tasks.py — background jobs
- exceptions.py — error definitions
- utils.py — ONLY for generic helpers
- ❌ helpers_mixed.py
- ❌ random_functions.py
🧪 11. Testing Architecture
- domain tested heavily with unit tests
- infrastructure tested with mocks
- API tested with integration tests
Part 2: Domain-Driven Design & Advanced Patterns
🔥 12. Domain-Driven Design (DDD) in Python
DDD is a system-design method focused on modelling business rules, not framework limitations.
- ✔ Code structure mirrors business structure
- ✔ Each domain is isolated
- ✔ Logic belongs to the domain, not to API or DB layers
- ✔ Domain objects represent real-world concepts
- Python's dynamic nature simplifies domain modelling
- Dataclasses + type hints make models clean
- Package isolation prevents circular imports
- Encourages clean business rules without tech dependencies
Goal: Domains should NOT depend on frameworks or external APIs. Only infrastructure depends on domains.
⚙️ 13. Domain Layer Responsibilities
Objects that have identity across time (e.g., User, Order).
Objects identified by value, not identity (e.g., Price, Email, Coordinates).
Logic that doesn't naturally belong to any one entity.
- ❌ database code
- ❌ API framework code
- ❌ external library calls
- ❌ logging / caching
- ❌ infrastructure details
This keeps the code clean, portable, and testable.
🧱 14. Infrastructure Layer (DB, Cache, External Systems)
Infrastructure is where all "real-world" systems live:
- actual SQL queries
- Redis connections
- HTTP clients
- integrations with Stripe, AWS, etc.
- ❌ contain business rules
- ❌ call domain services
Instead → infrastructure implements interfaces defined in the domain.
🧩 15. The API Layer
This is the "edge" of the app. Usually contains:
- Flask blueprints
- FastAPI routers
- Django views
- CLI commands (Click, Typer)
- Websocket handlers
- ✔ parsing requests
- ✔ converting domain errors → HTTP codes
- ✔ authentication
- ✔ response formatting
- ❌ contain business decisions
- ❌ contain SQL queries
- ❌ talk directly to infrastructure
- ❌ hold domain logic
Part 3: Real-World Architectures
🔥 16. The Three Master Architectures for Large Python Projects
There are 3 real-world architectures used by engineering teams once codebases reach 50K+ lines:
Clear vertical layers with strict dependency rules.
✔ 2. Clean/Hexagonal Architecture (enterprise-grade)
Domain is isolated and stable. Adapters wrap external systems. Application orchestrates flows.
Each feature is an "app" with its own mini-architecture inside.
- Many enterprise monoliths
⚙️ 17. How Django Organises Massive Codebases
- ✓ easy testing
- ✓ independent teams
- ✓ plugin marketplace (reusable apps)
Why this matters: If you model your website like this, you can scale to 200+ pages and thousands of functions without losing control.
🔥 18. How FastAPI Organises Modern Backend Projects
- Fast startup
- Domain + repository pattern
- Event-driven hooks
🎉 Final Conclusion
- ✔ Clean architecture
- ✔ Domain-driven design
- ✔ Layered module organization
- ✔ Plugin-based modular monolith
- ✔ Event-driven communication
- ✔ Dependency inversion
- ✔ Container-based dependency wiring
- ✔ API versioning for long-term stability
- ✔ Scaling to hundreds of modules
- ✔ How real companies structure Python systems
You now have the knowledge to architect and scale Python applications from startup MVPs to enterprise systems handling millions of users.
📋 Quick Reference — Module Architecture
Concept
What it means
__init__.py
Makes a directory a Python package
__all__ = [...]
Control what's exported from a module
from . import module
Relative import within a package
importlib.import_module()
Dynamic import at runtime
src/ layout
Best-practice project structure
You can now architect large Python codebases with proper packages, relative imports, and clean module boundaries.
Up next: Logging & Debugging — add professional observability to your Python applications.
Practice quiz
What traditionally makes a directory a regular Python package?
- Containing a setup.py file
- Being listed in sys.path
- Containing an __init__.py file
- Ending its name with .pkg
Answer: Containing an __init__.py file. A regular package is a folder containing an __init__.py; without it Python may treat the folder as a namespace package.
After importing a module once, where does Python cache it so it isn't re-executed?
- sys.modules
- sys.path
- __pycache__ only
- os.environ
Answer: sys.modules. Imported modules are cached in sys.modules; subsequent imports return the cached module object.
What does the __all__ list in a module control?
- The order methods run in
- The module's dependencies
- The Python version required
- Which names are treated as the module's public API (and exported by 'from module import *')
Answer: Which names are treated as the module's public API (and exported by 'from module import *'). __all__ defines the public API and the names exported on a wildcard import.
What is a circular import?
- A module that imports itself in a loop forever
- Two modules that import each other, which can cause runtime errors
- Importing the same module twice
- Importing a package without __init__.py
Answer: Two modules that import each other, which can cause runtime errors. When a.py imports b.py and b.py imports a.py, partial initialization can trigger ImportError/AttributeError.
Which is a recommended way to break a circular import?
- Introduce an interface/Protocol or move shared logic into a core module
- Duplicate the code in both modules
- Delete one of the __init__.py files
- Import everything at the top of every file
Answer: Introduce an interface/Protocol or move shared logic into a core module. Depending on an interface (Protocol) or shared core, or using local imports, removes the cycle.
In layered architecture, what is the #1 dependency-direction rule?
- High-level modules should depend directly on low-level modules
- Every module depends on every other module
- Low-level modules depend on high-level interfaces, not the reverse
- The API layer holds all the business logic
Answer: Low-level modules depend on high-level interfaces, not the reverse. Dependencies should point toward abstractions: low-level code implements interfaces defined by higher layers.
Which layer should contain the core business logic and domain rules?
- The API layer
- The domain/service layer
- The infrastructure/data layer
- The configuration layer
Answer: The domain/service layer. Business rules belong in the domain/service layer, kept independent of frameworks and databases.
What is the recommended rule about side effects when a module is imported?
- Open database connections at import time for speed
- Run all tests on import
- Print debug logs on every import
- Keep import-time side effects to zero (no DB connections, no heavy computation)
Answer: Keep import-time side effects to zero (no DB connections, no heavy computation). Modules execute on first import, so keep that fast and side-effect-free to avoid surprises and slow startup.
Per the lesson's naming conventions, where do business rules belong?
- models.py
- services.py
- routes.py
- exceptions.py
Answer: services.py. services.py holds business logic; models.py holds data classes and routes.py holds API endpoints.
What is the repository pattern's purpose in the infrastructure layer?
- To store git history
- To define HTTP routes
- To implement a data-access interface defined by the domain, hiding SQL/DB details
- To replace the domain layer entirely
Answer: To implement a data-access interface defined by the domain, hiding SQL/DB details. A repository implements a domain-defined interface for persistence, so the domain stays free of DB specifics.