Business Logic in Wrong Layer: Detection and Remediation
Business logic in the wrong layer is a failure pattern in AI-generated codebases where domain-specific operations — pricing calculations, permission checks, validation rules, data transformations — appear in architectural layers that should not contain them. Database queries in route handlers. Discount calculations in React components. Authorization logic in database models.
The structural mechanism: prompt-driven development resolves logic placement by asking "where does this fit in the current context?" — not "which layer is architecturally correct for this operation?" The result is that business logic migrates to wherever the developer was working when the prompt was written. Over time, the same business operation appears in multiple layers, implemented independently, diverging silently.
This page explains how to detect layer boundary violations with precision, how to interpret the findings, and what the remediation path looks like.
What We Observe
Business logic in the wrong layer presents with specific structural signatures across AI-generated codebases:
- Database queries in HTTP handlers — a route handler that directly queries the database rather than delegating to a repository or service layer; the handler is now responsible for both transport and data access
- Pricing and discount logic in UI components — a React component that calculates totals, applies discounts, or formats currency using business rules rather than displaying pre-calculated values from a service
- Authorization checks scattered across layers — permission validation present in route handlers, service methods, and UI components simultaneously, with no canonical location; a change to the permission model must be applied in all three places
- Data transformation duplicated across layers — the same API response shape constructed in three different places, each with slightly different field handling and edge case behavior
- Validation logic in the wrong layer — email validation in a database model, a service method, and a UI component — three implementations, three potential divergence points when the validation rule changes
Each violation is individually small. In aggregate, they represent the dissolution of the layer architecture that makes the system testable, maintainable, and safe to change. The most dangerous consequence: when a business rule changes, the team must find and update every layer where that rule was implemented — and the team may not know how many layers contain it.
The Layer Architecture Reference
The canonical layer structure for AI-generated web applications:
┌─────────────────────────────────────────┐
│ UI Layer (React components, templates) │ Display only — no business rules
├─────────────────────────────────────────┤
│ API / Controller Layer (route handlers)│ Transport only — no business logic
├─────────────────────────────────────────┤
│ Service Layer (business logic) │ All domain operations live here
├─────────────────────────────────────────┤
│ Repository Layer (data access) │ All DB queries live here
├─────────────────────────────────────────┤
│ Database / External Services │ Storage and infrastructure
└─────────────────────────────────────────┘
Allowed imports: each layer imports only from the layer directly below it.
Forbidden: skipping layers, importing upward, cross-domain imports.
A layer boundary violation is any import or logic placement that violates this model.
Detection
Python: Database Queries in Route Handlers
echo "=== DB queries in route/handler files (Python) ==="
find . \( -name "routes.py" -o -name "views.py" -o -name "handlers.py" \
-o -path "*/api/*.py" -o -path "*/routes/*.py" \) \
-not -path "*/test*" -not -path "*/__pycache__/*" 2>/dev/null | \
xargs grep -ln "\.query\(\|\.filter\(\|\.execute\(\|session\.\|db\." \
2>/dev/null
# Show the specific lines with context
find . \( -name "routes.py" -o -name "views.py" \) \
-not -path "*/test*" 2>/dev/null | \
xargs grep -n "\.query\(\|\.filter\(\|\.execute\(" 2>/dev/null | head -20
TypeScript: Direct Data Access in React Components
echo "=== Direct DB/API calls in UI components (TypeScript) ==="
# Components making direct data calls (not through hooks or services)
find src/components src/pages src/app -name "*.tsx" \
-not -name "*.test.tsx" 2>/dev/null | \
xargs grep -ln "fetch(\|axios\.\|supabase\.\|prisma\.\|\.from(" \
2>/dev/null | grep -v "api\|service\|hook\|use[A-Z]" | head -10
# Business logic keywords in UI layer
echo ""
echo "=== Business logic in UI components ==="
find src/components src/pages -name "*.tsx" 2>/dev/null | \
xargs grep -n "price\|discount\|tax\|total\|invoice\|permission\|role\|authorize" \
2>/dev/null | grep -v "import\|//\|display\|format\|label\|prop\|test" | head -20
Cross-Layer Validation and Authorization Logic
echo "=== Validation logic distribution across layers ==="
grep -rn "validate.*email\|email.*valid\|is_valid_email\|check.*email" \
--include="*.py" --include="*.ts" --include="*.tsx" \
. 2>/dev/null | grep -v "test\|spec\|mock\|import" | \
awk -F: '{print $1}' | sort | uniq -c | sort -rn | head -15
echo ""
echo "=== Authorization checks across layers ==="
grep -rn "hasPermission\|checkRole\|isAuthorized\|can_access\|require_role\|checkPermission" \
--include="*.py" --include="*.ts" --include="*.tsx" \
. 2>/dev/null | grep -v "test\|spec\|import" | \
awk -F: '{print $1}' | sort | uniq -c | sort -rn | head -15
Interpretation:
- Validation logic in 3+ files across different layers: critical — a rule change requires finding all implementations
- Authorization checks in both route handlers and UI components: critical — authorization can be bypassed by calling the service layer directly
- DB queries in route handlers: high — the handler is untestable without a live database
Interpretation Table
| Finding | Layer Violation | Risk |
|---|---|---|
| DB query in route handler | Transport → Data (skips Service + Repository) | High — untestable, tightly coupled |
| Business calc in UI component | Presentation → Domain (skips Service layer) | High — duplicates on every UI change |
| Auth check in DB model | Data → Domain (wrong direction) | Critical — auth bypassed by direct DB access |
| Validation in 3+ layers | No canonical layer | High — divergence inevitable on rule change |
| API call in React component | Presentation → Infrastructure (skips API layer) | Medium — hard to mock, hard to test |
| Pricing logic in route handler | Transport → Domain (skips Service layer) | High — pricing changes require handler edits |
Remediation Path
Step 1: Identify the Canonical Layer for Each Logic Type
Validation logic → Service layer (or dedicated Validator class)
Database queries → Repository layer (never in handlers or services directly)
Business calculations → Service layer (or dedicated Calculator class)
Authorization checks → Service layer (or dedicated Policy/Authorizer class)
Data transformation → Service layer or dedicated Mapper/Transformer
HTTP response shaping → Controller/Handler layer only
UI display formatting → UI component (display only — no business rules)
Step 2: Extract to the Canonical Layer
# ❌ Before: DB query + business logic in route handler
@app.post("/checkout")
async def checkout(request: CheckoutRequest, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == request.user_id).first()
discount = 0.1 if user.subscription_tier == "pro" else 0
total = request.amount * (1 - discount)
return {"total": total}
# ✅ After: handler delegates to service; service delegates to repository
@app.post("/checkout")
async def checkout(request: CheckoutRequest,
service: CheckoutService = Depends(get_checkout_service)):
result = service.execute(CheckoutRequest(
user_id=request.user_id,
amount=request.amount
))
return {"total": result.total}
# domains/checkout/calculate_total/service.py
class CheckoutService:
def __init__(self, repo: UserRepository) -> None:
self.repo = repo
def execute(self, request: CheckoutRequest) -> CheckoutResponse:
user = self.repo.get_by_id(request.user_id)
discount = 0.1 if user.subscription_tier == "pro" else 0
total = request.amount * (1 - discount)
return CheckoutResponse(total=round(total, 2))
# domains/user/repository.py
class UserRepository:
def __init__(self, db: Session) -> None:
self.db = db
def get_by_id(self, user_id: str) -> User:
return self.db.query(User).filter(User.id == user_id).first()
After extraction: the handler is testable with a mock service. The service is testable with a mock repository. The repository is testable with a test database. Each layer has a single responsibility and a single test surface.
Step 3: Enforce the Boundary
// .dependency-cruiser.js — enforce layer boundaries
module.exports = {
forbidden: [
{
name: "no-db-in-handlers",
severity: "error",
from: { path: "^src/(routes|handlers|controllers)/" },
to: { path: "^src/(models|db|database|prisma)/" }
},
{
name: "no-business-logic-in-ui",
severity: "error",
from: { path: "^src/components/" },
to: { path: "^src/(services|repositories|domains)/" }
}
]
};
How FP002 Interacts with Other Failure Patterns
Business logic in the wrong layer does not occur in isolation. It interacts with other failure patterns in a compounding pattern:
- FP001 (Oversized Files) — layer violations accumulate in the largest files; an oversized route handler is almost always a layer violation signal
- FP004 (Duplicate Business Logic) — when business logic appears in multiple layers, the same operation is implemented independently in each; FP002 is the structural cause of FP004
- FP006 (Circular Dependencies) — when the UI layer imports from the service layer and the service layer imports from the UI layer (e.g., for shared types), circular dependencies form; FP002 is a common precursor
- FP014 (Low Test Coverage) — logic in the wrong layer is structurally harder to test; a route handler with embedded DB queries requires a live database to test, which is why it is often left untested
Addressing FP002 — extracting logic to the canonical layer — directly reduces the severity of FP004, FP014, and the conditions that produce FP006.