Dependency Management
SPOC's dependency management system ensures modules are loaded in the correct order, preventing initialization failures and runtime errors. This guide explains how SPOC resolves dependencies, detects circular dependencies, and provides best practices for structuring your application.
Why Dependency Management Matters
In modular applications, components often depend on each other. For example:
- Views depend on models being loaded first
- Services depend on database connections being established
- API handlers depend on authentication middleware
Without proper dependency management, you might encounter:
- Initialization Errors: Accessing undefined modules or resources
- Runtime Failures: Missing dependencies causing crashes
- Unpredictable Behavior: Different initialization order in different runs
SPOC's dependency graph ensures deterministic, correct initialization order automatically.
Defining Dependencies in Schema
Dependencies are declared in the Schema object when creating your framework:
from spoc import Framework, Schema
from pathlib import Path
schema = Schema(
# List all modules to load
modules=["models", "views", "services"],
# Define dependencies between modules
dependencies={
"views": ["models"], # views depends on models
"services": ["models", "views"] # services depends on both
}
)
framework = Framework(
base_dir=Path(__file__).parent,
schema=schema
)
Dependency Rules
- Key: The module that has dependencies
- Value: List of modules it depends on
- Resolution: SPOC loads dependencies before dependents
- Transitive: Dependencies are resolved recursively
How Dependency Resolution Works
SPOC uses a Directed Acyclic Graph (DAG) with topological sorting to determine the correct initialization order.
Example: Three-Module System
schema = Schema(
modules=["models", "views", "services"],
dependencies={
"views": ["models"],
"services": ["models", "views"]
}
)
Dependency Graph:
graph LR
models[models] --> views[views]
models --> services[services]
views --> services
style models fill:#e1f5ff
style views fill:#fff4e1
style services fill:#ffe1f5
Initialization Order:
sequenceDiagram
participant Framework
participant Models
participant Views
participant Services
Framework->>Models: 1. Load & Initialize
Models-->>Framework: Ready
Framework->>Views: 2. Load & Initialize
Views-->>Framework: Ready
Framework->>Services: 3. Load & Initialize
Services-->>Framework: Ready
Note over Framework: All modules loaded in correct order
Result: models → views → services
The DependencyGraph Class
SPOC's dependency resolution is powered by the DependencyGraph class in spoc.core.utils.
Core Methods
add_node(node)
Adds a node to the graph representing a module.
from spoc.core.utils import DependencyGraph
graph = DependencyGraph[str]()
graph.add_node("models")
graph.add_node("views")
add_edge(from_node, to_node)
Creates a directed edge from one node to another, indicating that to_node depends on from_node.
# views depends on models
graph.add_edge("models", "views")
# services depends on both models and views
graph.add_edge("models", "services")
graph.add_edge("views", "services")
topological_sort()
Performs topological sort to determine initialization order.
# Returns modules in dependency order
order = graph.topological_sort()
# Result: ["models", "views", "services"]
Algorithm: Uses Kahn's algorithm with in-degree calculation.
Time Complexity: O(V + E) where V = vertices (modules), E = edges (dependencies)
reversed()
Creates a new graph with all edges reversed, used for shutdown order.
# Reverse the graph for shutdown
shutdown_graph = graph.reversed()
shutdown_order = shutdown_graph.topological_sort()
# Result: ["services", "views", "models"]
Circular Dependency Detection
SPOC automatically detects circular dependencies and raises CircularDependencyError with a detailed error message.
Example: Circular Dependency
schema = Schema(
modules=["models", "views", "services"],
dependencies={
"views": ["models"],
"models": ["services"], # Creates a cycle!
"services": ["views"]
}
)
# This will raise CircularDependencyError
framework = Framework(base_dir=Path("."), schema=schema)
Problem Visualization:
graph LR
models[models] --> services[services]
services --> views[views]
views --> models
style models fill:#ffcccc
style views fill:#ffcccc
style services fill:#ffcccc
Error Message:
How Detection Works
During topological_sort():
- Calculate in-degree for each node
- Process nodes with in-degree of 0
- If unprocessed nodes remain, a cycle exists
- Perform depth-first search to identify the exact cycle
- Raise
CircularDependencyErrorwith cycle path
Dependency Resolution Examples
Example 1: Linear Dependencies
Simple chain where each module depends on the previous one.
schema = Schema(
modules=["database", "models", "services", "api"],
dependencies={
"models": ["database"],
"services": ["models"],
"api": ["services"]
}
)
Graph:
graph LR
database[database] --> models[models]
models --> services[services]
services --> api[api]
style database fill:#e1f5ff
style models fill:#fff4e1
style services fill:#ffe1f5
style api fill:#e1ffe1
Initialization Order: database → models → services → api
Shutdown Order: api → services → models → database
Example 2: Diamond Dependencies
Multiple modules depend on a common base module.
schema = Schema(
modules=["database", "models", "cache", "api"],
dependencies={
"models": ["database"],
"cache": ["database"],
"api": ["models", "cache"]
}
)
Graph:
graph TD
database[database] --> models[models]
database --> cache[cache]
models --> api[api]
cache --> api
style database fill:#e1f5ff
style models fill:#fff4e1
style cache fill:#fff4e1
style api fill:#ffe1f5
Possible Initialization Orders:
database→models→cache→apidatabase→cache→models→api
Both are valid. SPOC guarantees database loads first and api loads last.
Example 3: Complex Multi-Layer Application
Real-world application with multiple layers and dependencies.
schema = Schema(
modules=[
"config",
"database",
"cache",
"models",
"repositories",
"services",
"middleware",
"views",
"api"
],
dependencies={
"database": ["config"],
"cache": ["config"],
"models": ["database"],
"repositories": ["models", "cache"],
"services": ["repositories"],
"middleware": ["services"],
"views": ["services"],
"api": ["middleware", "views"]
}
)
Graph:
graph TD
config[config] --> database[database]
config --> cache[cache]
database --> models[models]
models --> repositories[repositories]
cache --> repositories
repositories --> services[services]
services --> middleware[middleware]
services --> views[views]
middleware --> api[api]
views --> api
style config fill:#e1f5ff
style database fill:#fff4e1
style cache fill:#fff4e1
style models fill:#ffe1f5
style repositories fill:#e1ffe1
style services fill:#e1f5ff
style middleware fill:#fff4e1
style views fill:#fff4e1
style api fill:#ffe1f5
Initialization Flow:
flowchart TD
Start([Framework Startup]) --> Load1[Load: config]
Load1 --> Load2[Load: database, cache]
Load2 --> Load3[Load: models]
Load3 --> Load4[Load: repositories]
Load4 --> Load5[Load: services]
Load5 --> Load6[Load: middleware, views]
Load6 --> Load7[Load: api]
Load7 --> Done([Ready])
style Start fill:#e1f5ff
style Done fill:#e1ffe1
Startup and Shutdown Order
SPOC ensures proper initialization and cleanup by using the dependency graph in both directions.
Startup Order
Modules are initialized in dependency order (leaves to root):
framework = Framework(base_dir=Path("."), schema=schema)
# Automatically calls startup()
# Internally:
# 1. Build dependency graph
# 2. Perform topological sort
# 3. Load modules in order: models → views → services
# 4. Call initialize() on each module
Shutdown Order
Modules are torn down in reverse dependency order (root to leaves):
framework.shutdown()
# Internally:
# 1. Reverse the dependency graph
# 2. Perform topological sort on reversed graph
# 3. Teardown modules in order: services → views → models
# 4. Call teardown() on each module
Example:
sequenceDiagram
participant Framework
participant Models
participant Views
participant Services
Note over Framework,Services: STARTUP
Framework->>Models: initialize()
Framework->>Views: initialize()
Framework->>Services: initialize()
Note over Framework,Services: Application runs...
Note over Framework,Services: SHUTDOWN
Framework->>Services: teardown()
Framework->>Views: teardown()
Framework->>Models: teardown()
Best Practices
1. Keep Dependencies Minimal
Only declare dependencies you actually need.
Bad:
# views doesn't actually use services
dependencies={
"views": ["models", "services"],
"services": ["models"]
}
Good:
2. Avoid Circular Dependencies
Design your architecture to prevent circular dependencies.
Bad:
graph LR
models[models] --> services[services]
services --> models
style models fill:#ffcccc
style services fill:#ffcccc
Good:
graph LR
models[models] --> repositories[repositories]
repositories --> services[services]
style models fill:#e1f5ff
style repositories fill:#fff4e1
style services fill:#ffe1f5
3. Layer Your Modules
Follow a layered architecture:
Data Layer (models, database)
↓
Logic Layer (repositories, services)
↓
Presentation Layer (views, api)
Implementation:
schema = Schema(
modules=["models", "repositories", "services", "views", "api"],
dependencies={
# Data layer
"repositories": ["models"],
# Logic layer
"services": ["repositories"],
# Presentation layer
"views": ["services"],
"api": ["services"]
}
)
4. Use Clear Naming Conventions
Name modules to reflect their layer and purpose:
database.py- Database connectionsmodels.py- Data modelsrepositories.py- Data access layerservices.py- Business logicviews.py- Presentation logicapi.py- API endpoints
5. Document Complex Dependencies
For complex applications, document why dependencies exist:
schema = Schema(
modules=["cache", "models", "services"],
dependencies={
# services needs models for data structures
# services needs cache for performance optimization
"services": ["models", "cache"]
}
)
6. Test Dependency Order
Write tests to verify initialization order:
import pytest
from spoc.core.utils import DependencyGraph
def test_dependency_order():
graph = DependencyGraph[str]()
graph.add_edge("models", "views")
graph.add_edge("models", "services")
graph.add_edge("views", "services")
order = graph.topological_sort()
# Verify models loads first
assert order.index("models") < order.index("views")
assert order.index("models") < order.index("services")
# Verify services loads last
assert order.index("services") > order.index("views")
assert order.index("services") > order.index("models")
7. Handle Missing Dependencies Gracefully
Use mode="loose" for optional dependencies:
# Strict mode - all modules must exist
framework = Framework(
base_dir=Path("."),
schema=schema,
mode="strict" # Raises error if module missing
)
# Loose mode - allows missing modules
framework = Framework(
base_dir=Path("."),
schema=schema,
mode="loose" # Skips missing modules
)
Common Pitfalls
Pitfall 1: Forgetting Transitive Dependencies
Problem: Not declaring indirect dependencies.
# services uses repositories, which uses models
# But you only declare direct dependency
dependencies={
"repositories": ["models"],
"services": ["repositories"] # Missing models!
}
Solution: SPOC handles transitive dependencies automatically. Just declare direct dependencies:
dependencies={
"repositories": ["models"],
"services": ["repositories"] # SPOC knows services needs models
}
Pitfall 2: Dependency on Self
Problem: Module depending on itself.
Solution: Remove self-dependencies. They're always errors.
Pitfall 3: Implicit Dependencies
Problem: Using imports without declaring dependencies.
# views.py
from .models import User # Implicit dependency!
# But schema doesn't declare it
dependencies={} # Missing: "views": ["models"]
Solution: Always declare dependencies explicitly:
Advanced Usage
Custom Dependency Graphs
Create custom dependency graphs for specialized use cases:
from spoc.core.utils import DependencyGraph
# Build a custom task dependency graph
tasks = DependencyGraph[str]()
tasks.add_edge("fetch_data", "process_data")
tasks.add_edge("process_data", "save_results")
tasks.add_edge("fetch_data", "log_activity")
# Get execution order
execution_order = tasks.topological_sort()
# ["fetch_data", "process_data", "log_activity", "save_results"]
for task in execution_order:
print(f"Running task: {task}")
Dynamic Dependencies
Add dependencies at runtime based on configuration:
def build_schema(config):
modules = ["core", "api"]
dependencies = {"api": ["core"]}
# Add optional modules based on config
if config.get("enable_cache"):
modules.append("cache")
dependencies["api"] = ["core", "cache"]
if config.get("enable_auth"):
modules.append("auth")
dependencies["api"].append("auth")
return Schema(modules=modules, dependencies=dependencies)
Summary
SPOC's dependency management system:
- Ensures correct initialization order using topological sorting
- Detects circular dependencies automatically with detailed error messages
- Handles shutdown properly by reversing the dependency graph
- Supports complex architectures with multi-layer dependencies
- Provides predictable behavior with deterministic ordering
By following best practices and understanding how SPOC resolves dependencies, you can build robust, maintainable applications with confidence that modules will always load in the correct order.
Next Steps
- Learn about Lifecycle Hooks: Lifecycle Hooks Guide
- Explore the Framework API: Framework API Reference
- Study the Importer: Importer Guide
- Review Examples: Check the
examples/directory for real-world patterns