Framework-Driven Design Principles
Architect applications by embracing framework constraints—Inversion of Control (IoC), Dependency Injection (DI), lifecycle hooks, and plugin systems—rather than fighting them. This skill applies the SOLID principles (especially DIP — Dependency Inversion Principle) to transform rigid requirements into extensible, maintainable architectures.
TL;DR Checklist
TL;DR for Code Generation
- Use Protocol/abstract base classes (Python typing.Protocol, Go interfaces, TypeScript interface) for all DI contracts — never concrete types in signatures
- Always inject dependencies via constructor or parameter injection; never use
new, globals, or module-level singletons inside domain logic
- Plugin Execute methods must accept a context object and return wrapped errors using
fmt.Errorf("extension %s: %w", name, err) (Go) or equivalent error chaining
- Middleware/lifecycle handlers must implement try/finally (or defer/ensure) to guarantee resource cleanup on both success and failure paths
- All external I/O (databases, caches, HTTP clients) must be abstracted behind injected adapters — never instantiate connection objects inline
- Configuration-driven behavior uses typed config modules; avoid magic strings for routing keys or feature flags
When to Use
Use this skill when:
- Architecting a new application with a modern framework (React/Next.js, FastAPI, Spring Boot 3+, Rails 7+)
- Refactoring legacy code that bypasses framework features (e.g., global state, manual dependency wiring)
- Designing plugin systems or extension points for a developer-facing SDK
- Integrating third-party frameworks where constraints must be respected to avoid upgrade friction
When NOT to Use
Avoid this skill for:
- Standalone scripts or CLI tools with no lifecycle or DI requirements (use simple procedural design)
- Performance-critical inner loops where framework overhead is unacceptable (drop to lower-level abstractions)
- Situations requiring tight coupling to legacy systems that cannot adapt to IoC/DI patterns
Core Workflow
Map Framework Lifecycle — Identify the exact phase boundaries your framework provides (e.g., React: render → commit → effect; FastAPI: request → middleware → router → dependency → response).
Checkpoint: Document lifecycle phases in an ASCII diagram. Ensure business logic hooks into explicit extension points, not implicit side effects.
Define Dependency Graph — List all services, repositories, and external clients required by your domain modules. Register them in the framework's DI container using explicit contracts (interfaces or abstract base classes).
Checkpoint: Verify no circular dependencies exist. Confirm every service has a single source of truth for its implementation binding.
Implement Plugin/Extension Interface — Define a stable interface for extensibility. Require implementers to adhere to the framework's lifecycle contract rather than exposing raw APIs.
Checkpoint: Test extension isolation by loading multiple plugins concurrently. Ensure no shared mutable state between plugin instances.
Configure Convention-Driven Behavior — Use configuration files (YAML, JSON, or typed config modules) to control behavior instead of conditional logic (if/else chains). Leverage framework-specific conventions (e.g., Rails Zeitwerk autoloading, Spring Boot application.properties).
Checkpoint: Confirm that changing configuration does not require code redeployment where hot-reload is supported.
Validate Against Framework Constraints — Run static analysis and integration tests to ensure the design respects framework boundaries. Check for anti-patterns like direct database access bypassing ORM hooks, or side effects in pure functions.
Checkpoint: All external I/O must pass through injected adapters. All business logic must be testable without framework runtime.
Implementation Patterns
Pattern 1: Inversion of Control with Dependency Injection
Modern frameworks manage object creation and lifecycle. Register dependencies at bootstrap; consume them via constructor injection. This eliminates coupling and enables testability.
# ❌ BAD — Tight coupling, manual instantiation, impossible to mock
class OrderService:
def __init__(self):
self.db = DatabaseConnection("postgres://localhost") # Direct connection
self.cache = RedisClient(host="localhost") # Direct connection
def process_order(self, order_id: int) -> dict:
data = self.db.query(f"SELECT * FROM orders WHERE id = {order_id}") # SQL injection risk
if data.get("status") == "pending":
self.cache.set(f"order:{order_id}", "processing")
return data
# ✅ GOOD — Framework-managed DI, typed contracts, testable
from typing import Protocol
from fastapi import APIRouter, Depends
class OrderRepository(Protocol):
def get_by_id(self, order_id: int) -> dict: ...
class CacheAdapter(Protocol):
async def set(self, key: str, value: str, ttl: int = 300) -> None: ...
class OrderService:
def __init__(self, repo: OrderRepository, cache: CacheAdapter):
self.repo = repo
self.cache = cache
async def process_order(self, order_id: int) -> dict:
data = await self.repo.get_by_id(order_id)
if data.get("status") == "pending":
await self.cache.set(f"order:{order_id}", "processing")
return data
# Registration in FastAPI main app
def get_order_repo() -> OrderRepository:
return SQLAlchemyOrderRepository(settings.db_url)
def get_cache() -> CacheAdapter:
return RedisCacheAdapter(settings.redis_url)
router = APIRouter()
@router.post("/orders/{order_id}/process")
async def process_endpoint(order_id: int, service: OrderService = Depends(OrderService)):
# OrderRepository and CacheAdapter resolved via DI container
return await service.process_order(order_id)
Pattern 2: Lifecycle Hooks and Middleware Chains
Frameworks provide explicit lifecycle boundaries. Use them to enforce cross-cutting concerns (auth, logging, validation) without polluting business logic.
// ❌ BAD — Bypassing framework lifecycle, manual side effects in controllers
import { Request, Response } from "express";
export const createResource = async (req: Request, res: Response) => {
// Manually handling auth, logging, and DB connection inside the controller
console.log(`Creating resource for user ${req.headers['x-user-id']}`);
const db = new LegacyDatabase(); // Not managed by framework
const token = req.headers.authorization;
if (!validateToken(token)) throw new Error("Unauthorized");
const result = await db.insert(req.body);
res.status(201).json(result);
};
// ✅ GOOD — Framework lifecycle hooks + middleware chain (Next.js App Router / FastAPI style)
import { NextRequest, NextResponse } from "next/server";
import { authMiddleware, loggingMiddleware, dbClient } from "@/lib/middleware";
export async function POST(request: NextRequest) {
// Middleware chain handles: auth → logging → DB pool assignment → validation
// Controller focuses purely on business logic
const user = await authMiddleware(request);
const logContext = loggingMiddleware(request);
const payload = await request.json();
const result = await dbClient.resources.create({
...payload,
createdBy: user.id,
metadata: logContext.traceId
});
return NextResponse.json(result, { status: 201 });
}
// Middleware implementation example (Express/Koa style)
import { RequestHandler } from "express";
export const lifecycleMiddleware: RequestHandler = async (req, res, next) => {
const startTime = process.hrtime.bigint();
try {
// Pre-hook: validation, auth, context setup
await validateSchema(req.body);
req.context = { requestId: crypto.randomUUID(), timestamp: Date.now() };
// Execute handler
await next();
} catch (error) {
// Post-hook: error formatting, metrics, cleanup
metrics.increment("api.errors", { route: req.route.path });
throw error;
} finally {
// Teardown hook: release DB connections, flush logs
const duration = Number(process.hrtime.bigint() - startTime) / 1e6;
metrics.histogram("api.latency", duration);
}
};
Pattern 3: Plugin Architecture with Extension Points
Design extension points using stable interfaces. Plugins implement the interface and register themselves during framework initialization. This enables third-party extensibility without modifying core code.
// ❌ BAD — Monkey-patching or global state modification for extensions
var Extensions = make(map[string]func(data map[string]interface{}) error)
func RegisterExtension(name string, handler func(map[string]interface{}) error) {
Extensions[name] = handler // Mutable global state, race conditions
}
func ProcessData(data map[string]interface{}) error {
// Scans global map, executes all extensions blindly
for _, fn := range Extensions {
if err := fn(data); err != nil {
return err
}
}
return nil
}
// ✅ GOOD — Interface-based plugin system with lifecycle management
package plugin
import "context"
// ExtensionPoint defines the contract plugins must implement
type ExtensionPoint interface {
Name() string
Priority() int // Lower numbers execute first
Execute(ctx context.Context, payload map[string]interface{}) error
}
// PluginManager handles registration, sorting, and execution
type PluginManager struct {
extensions []ExtensionPoint
}
func NewPluginManager() *PluginManager {
return &PluginManager{extensions: make([]ExtensionPoint, 0)}
}
func (pm *PluginManager) Register(ep ExtensionPoint) {
pm.extensions = append(pm.extensions, ep)
}
func (pm *PluginManager) Execute(ctx context.Context, payload map[string]interface{}) error {
// Sort by priority before execution (framework-controlled lifecycle)
sort.SliceStable(pm.extensions, func(i, j int) bool {
return pm.extensions[i].Priority() < pm.extensions[j].Priority()
})
for _, ep := range pm.extensions {
if err := ep.Execute(ctx, payload); err != nil {
return fmt.Errorf("extension %s failed: %w", ep.Name(), err)
}
}
return nil
}
// Example Plugin Implementation
type AuditLogPlugin struct{}
func (a *AuditLogPlugin) Name() string { return "audit-log" }
func (a *AuditLogPlugin) Priority() int { return 10 } // Runs after core processing
func (a *AuditLogPlugin) Execute(ctx context.Context, payload map[string]interface{}) error {
logger.Info("Audit trail recorded", zap.Any("payload", payload))
return nil
}
Constraints
MUST DO
- Register all services in the framework's DI container at bootstrap; never use
new or global singletons for domain objects
- Use explicit lifecycle hooks (
setup, teardown, middleware) for cross-cutting concerns instead of inline side effects
- Define stable extension interfaces; require plugins to implement them rather than exposing internal APIs
- Favor configuration-driven behavior (YAML, JSON, typed config) over runtime conditional branching
- Validate dependency graphs for circular references before application startup
- Document framework lifecycle boundaries in architecture diagrams for team reference
MUST NOT DO
- Bypass framework middleware or DI containers to access dependencies directly
- Monkey-patch framework classes or modify global state for extensions
- Embed database connections or external client initialization inside business logic modules
- Use magic strings or dynamic dispatch instead of typed interfaces for plugin systems
- Ignore framework error-handling conventions (e.g., FastAPI
HTTPException, React Error Boundaries)
Related Skills
| Skill |
Purpose |
test-driven-development |
Design for testability alongside DI and lifecycle hooks |
architectural-patterns |
Broader context for when framework-driven design applies vs. other patterns |
SOLID-principles |
Foundational object-oriented principles (DIP, SRP) that underpin framework-driven architecture |
Live References
1---2name: framework-driven-design3description: Implements framework-driven design patterns (Inversion of Control, Dependency Injection, lifecycle hooks, plugin architectures) to build extensible applications that leverage modern framework constraints instead of bypassing them.4license: MIT5---67891011# Framework-Driven Design Principles1213Architect applications by embracing framework constraints—Inversion of Control (IoC), Dependency Injection (DI), lifecycle hooks, and plugin systems—rather than fighting them. This skill applies the SOLID principles (especially DIP — Dependency Inversion Principle) to transform rigid requirements into extensible, maintainable architectures.1415## TL;DR Checklist1617- [ ] Audit framework lifecycle before writing business logic18- [ ] Register services in DI container at application bootstrap19- [ ] Use explicit lifecycle hooks (mount, update, teardown) instead of manual state management20- [ ] Extend via plugin interfaces or middleware chains, not monkey-patching21- [ ] Favor configuration-driven behavior over code branching22- [ ] Validate framework constraints against architecture diagrams during design review232425## TL;DR for Code Generation2627- Use Protocol/abstract base classes (Python typing.Protocol, Go interfaces, TypeScript interface) for all DI contracts — never concrete types in signatures28- Always inject dependencies via constructor or parameter injection; never use `new`, globals, or module-level singletons inside domain logic29- Plugin Execute methods must accept a context object and return wrapped errors using `fmt.Errorf("extension %s: %w", name, err)` (Go) or equivalent error chaining30- Middleware/lifecycle handlers must implement try/finally (or defer/ensure) to guarantee resource cleanup on both success and failure paths31- All external I/O (databases, caches, HTTP clients) must be abstracted behind injected adapters — never instantiate connection objects inline32- Configuration-driven behavior uses typed config modules; avoid magic strings for routing keys or feature flags33---3435## When to Use3637Use this skill when:3839- Architecting a new application with a modern framework (React/Next.js, FastAPI, Spring Boot 3+, Rails 7+)40- Refactoring legacy code that bypasses framework features (e.g., global state, manual dependency wiring)41- Designing plugin systems or extension points for a developer-facing SDK42- Integrating third-party frameworks where constraints must be respected to avoid upgrade friction4344## When NOT to Use4546Avoid this skill for:4748- Standalone scripts or CLI tools with no lifecycle or DI requirements (use simple procedural design)49- Performance-critical inner loops where framework overhead is unacceptable (drop to lower-level abstractions)50- Situations requiring tight coupling to legacy systems that cannot adapt to IoC/DI patterns5152---5354## Core Workflow55561. **Map Framework Lifecycle** — Identify the exact phase boundaries your framework provides (e.g., React: `render` → `commit` → `effect`; FastAPI: `request` → `middleware` → `router` → `dependency` → `response`).57 **Checkpoint:** Document lifecycle phases in an ASCII diagram. Ensure business logic hooks into explicit extension points, not implicit side effects.58592. **Define Dependency Graph** — List all services, repositories, and external clients required by your domain modules. Register them in the framework's DI container using explicit contracts (interfaces or abstract base classes).60 **Checkpoint:** Verify no circular dependencies exist. Confirm every service has a single source of truth for its implementation binding.61623. **Implement Plugin/Extension Interface** — Define a stable interface for extensibility. Require implementers to adhere to the framework's lifecycle contract rather than exposing raw APIs.63 **Checkpoint:** Test extension isolation by loading multiple plugins concurrently. Ensure no shared mutable state between plugin instances.64654. **Configure Convention-Driven Behavior** — Use configuration files (YAML, JSON, or typed config modules) to control behavior instead of conditional logic (`if/else` chains). Leverage framework-specific conventions (e.g., Rails Zeitwerk autoloading, Spring Boot `application.properties`).66 **Checkpoint:** Confirm that changing configuration does not require code redeployment where hot-reload is supported.67685. **Validate Against Framework Constraints** — Run static analysis and integration tests to ensure the design respects framework boundaries. Check for anti-patterns like direct database access bypassing ORM hooks, or side effects in pure functions.69 **Checkpoint:** All external I/O must pass through injected adapters. All business logic must be testable without framework runtime.7071---7273## Implementation Patterns7475### Pattern 1: Inversion of Control with Dependency Injection7677Modern frameworks manage object creation and lifecycle. Register dependencies at bootstrap; consume them via constructor injection. This eliminates coupling and enables testability.7879```python80# ❌ BAD — Tight coupling, manual instantiation, impossible to mock81class OrderService:82 def __init__(self):83 self.db = DatabaseConnection("postgres://localhost") # Direct connection84 self.cache = RedisClient(host="localhost") # Direct connection8586 def process_order(self, order_id: int) -> dict:87 data = self.db.query(f"SELECT * FROM orders WHERE id = {order_id}") # SQL injection risk88 if data.get("status") == "pending":89 self.cache.set(f"order:{order_id}", "processing")90 return data9192# ✅ GOOD — Framework-managed DI, typed contracts, testable93from typing import Protocol94from fastapi import APIRouter, Depends9596class OrderRepository(Protocol):97 def get_by_id(self, order_id: int) -> dict: ...9899class CacheAdapter(Protocol):100 async def set(self, key: str, value: str, ttl: int = 300) -> None: ...101102class OrderService:103 def __init__(self, repo: OrderRepository, cache: CacheAdapter):104 self.repo = repo105 self.cache = cache106107 async def process_order(self, order_id: int) -> dict:108 data = await self.repo.get_by_id(order_id)109 if data.get("status") == "pending":110 await self.cache.set(f"order:{order_id}", "processing")111 return data112113# Registration in FastAPI main app114def get_order_repo() -> OrderRepository:115 return SQLAlchemyOrderRepository(settings.db_url)116117def get_cache() -> CacheAdapter:118 return RedisCacheAdapter(settings.redis_url)119120router = APIRouter()121@router.post("/orders/{order_id}/process")122async def process_endpoint(order_id: int, service: OrderService = Depends(OrderService)):123 # OrderRepository and CacheAdapter resolved via DI container124 return await service.process_order(order_id)125```126127### Pattern 2: Lifecycle Hooks and Middleware Chains128129Frameworks provide explicit lifecycle boundaries. Use them to enforce cross-cutting concerns (auth, logging, validation) without polluting business logic.130131```typescript132// ❌ BAD — Bypassing framework lifecycle, manual side effects in controllers133import { Request, Response } from "express";134135export const createResource = async (req: Request, res: Response) => {136 // Manually handling auth, logging, and DB connection inside the controller137 console.log(`Creating resource for user ${req.headers['x-user-id']}`);138 const db = new LegacyDatabase(); // Not managed by framework139 const token = req.headers.authorization;140 if (!validateToken(token)) throw new Error("Unauthorized");141 142 const result = await db.insert(req.body);143 res.status(201).json(result);144};145146// ✅ GOOD — Framework lifecycle hooks + middleware chain (Next.js App Router / FastAPI style)147import { NextRequest, NextResponse } from "next/server";148import { authMiddleware, loggingMiddleware, dbClient } from "@/lib/middleware";149150export async function POST(request: NextRequest) {151 // Middleware chain handles: auth → logging → DB pool assignment → validation152 // Controller focuses purely on business logic153 const user = await authMiddleware(request);154 const logContext = loggingMiddleware(request);155 156 const payload = await request.json();157 const result = await dbClient.resources.create({158 ...payload,159 createdBy: user.id,160 metadata: logContext.traceId161 });162163 return NextResponse.json(result, { status: 201 });164}165166// Middleware implementation example (Express/Koa style)167import { RequestHandler } from "express";168169export const lifecycleMiddleware: RequestHandler = async (req, res, next) => {170 const startTime = process.hrtime.bigint();171 172 try {173 // Pre-hook: validation, auth, context setup174 await validateSchema(req.body);175 req.context = { requestId: crypto.randomUUID(), timestamp: Date.now() };176 177 // Execute handler178 await next();179 } catch (error) {180 // Post-hook: error formatting, metrics, cleanup181 metrics.increment("api.errors", { route: req.route.path });182 throw error;183 } finally {184 // Teardown hook: release DB connections, flush logs185 const duration = Number(process.hrtime.bigint() - startTime) / 1e6;186 metrics.histogram("api.latency", duration);187 }188};189```190191### Pattern 3: Plugin Architecture with Extension Points192193Design extension points using stable interfaces. Plugins implement the interface and register themselves during framework initialization. This enables third-party extensibility without modifying core code.194195```go196// ❌ BAD — Monkey-patching or global state modification for extensions197var Extensions = make(map[string]func(data map[string]interface{}) error)198199func RegisterExtension(name string, handler func(map[string]interface{}) error) {200 Extensions[name] = handler // Mutable global state, race conditions201}202203func ProcessData(data map[string]interface{}) error {204 // Scans global map, executes all extensions blindly205 for _, fn := range Extensions {206 if err := fn(data); err != nil {207 return err208 }209 }210 return nil211}212213// ✅ GOOD — Interface-based plugin system with lifecycle management214package plugin215216import "context"217218// ExtensionPoint defines the contract plugins must implement219type ExtensionPoint interface {220 Name() string221 Priority() int // Lower numbers execute first222 Execute(ctx context.Context, payload map[string]interface{}) error223}224225// PluginManager handles registration, sorting, and execution226type PluginManager struct {227 extensions []ExtensionPoint228}229230func NewPluginManager() *PluginManager {231 return &PluginManager{extensions: make([]ExtensionPoint, 0)}232}233234func (pm *PluginManager) Register(ep ExtensionPoint) {235 pm.extensions = append(pm.extensions, ep)236}237238func (pm *PluginManager) Execute(ctx context.Context, payload map[string]interface{}) error {239 // Sort by priority before execution (framework-controlled lifecycle)240 sort.SliceStable(pm.extensions, func(i, j int) bool {241 return pm.extensions[i].Priority() < pm.extensions[j].Priority()242 })243244 for _, ep := range pm.extensions {245 if err := ep.Execute(ctx, payload); err != nil {246 return fmt.Errorf("extension %s failed: %w", ep.Name(), err)247 }248 }249 return nil250}251252// Example Plugin Implementation253type AuditLogPlugin struct{}254255func (a *AuditLogPlugin) Name() string { return "audit-log" }256func (a *AuditLogPlugin) Priority() int { return 10 } // Runs after core processing257func (a *AuditLogPlugin) Execute(ctx context.Context, payload map[string]interface{}) error {258 logger.Info("Audit trail recorded", zap.Any("payload", payload))259 return nil260}261```262263---264265## Constraints266267### MUST DO268- Register all services in the framework's DI container at bootstrap; never use `new` or global singletons for domain objects269- Use explicit lifecycle hooks (`setup`, `teardown`, `middleware`) for cross-cutting concerns instead of inline side effects270- Define stable extension interfaces; require plugins to implement them rather than exposing internal APIs271- Favor configuration-driven behavior (YAML, JSON, typed config) over runtime conditional branching272- Validate dependency graphs for circular references before application startup273- Document framework lifecycle boundaries in architecture diagrams for team reference274275### MUST NOT DO276- Bypass framework middleware or DI containers to access dependencies directly277- Monkey-patch framework classes or modify global state for extensions278- Embed database connections or external client initialization inside business logic modules279- Use magic strings or dynamic dispatch instead of typed interfaces for plugin systems280- Ignore framework error-handling conventions (e.g., FastAPI `HTTPException`, React Error Boundaries)281282---283284## Related Skills285286| Skill | Purpose |287|---|---|288| `test-driven-development` | Design for testability alongside DI and lifecycle hooks |289| `architectural-patterns` | Broader context for when framework-driven design applies vs. other patterns |290| `SOLID-principles` | Foundational object-oriented principles (DIP, SRP) that underpin framework-driven architecture |291292## Live References293294- [FastAPI Dependency Injection System](https://fastapi.tiangolo.com/tutorial/dependencies/)295- [React Server Components & Lifecycle](https://react.dev/reference/react)296- [Spring Boot 3 Auto-Configuration](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.externalized-configuration)297- [Express.js Middleware Architecture](https://expressjs.com/en/guide/using-middleware.html)