ASAASA Standard

Slice Isolation: Modular Architecture for AI-Generated Codebases

Slice isolation is a structural technique for AI-generated codebases that organizes code by operation rather than by type. Instead of grouping all services together, all repositories together, and all handlers together, each discrete business operation — calculate_overage, send_invoice, validate_email — lives in its own isolated directory containing all the code it needs: handler, service, repository, types, and tests.

The technique directly addresses the structural failure mode of prompt-driven development: when code is organized by type (all services in one folder), each prompt session adds logic to the largest existing service file — because that file contains the most related logic and is therefore the most convenient location. Files grow. Layer boundaries erode. Duplicate logic accumulates. Slice isolation removes the structural incentive for accumulation by giving each operation its own dedicated space.

This page explains the slice structure, how to implement it in Python and TypeScript codebases, and what the expected structural outcome looks like.


What Slice Isolation Solves

Slice isolation directly addresses the structural conditions that produce hidden technical debt and architecture drift in AI-generated codebases:

  • Oversized files — when all services live in services.py or service.ts, every new operation adds to the same file; slice isolation gives each operation its own file with a hard size ceiling
  • Duplicate business logic — when operations are scattered across large files, new sessions cannot find existing implementations; slice isolation makes each operation discoverable by directory name
  • Business logic in wrong layer — when all code for an operation is co-located, the layer boundaries within the operation are explicit; a handler that imports from its own repository is a visible violation
  • Shared utils overuse — when operations are isolated, shared utilities are only extracted when genuinely needed by multiple operations; the default is duplication within the slice, not premature abstraction

The Slice Structure

A slice is a directory that contains all the code for a single discrete business operation. The directory name is the operation name. The contents are standardized:

domains/
  billing/
    calculate_overage/          ← one slice = one operation
      __init__.py               ← public interface (exports only)
      handler.py                ← HTTP handler / route (transport layer)
      service.py                ← business logic (domain layer)
      repository.py             ← data access (data layer)
      types.py                  ← request/response types
      test_service.py           ← unit tests for service
      test_handler.py           ← integration tests for handler
    send_invoice/               ← separate slice, no shared state
      __init__.py
      handler.py
      service.py
      repository.py
      types.py
      test_service.py
  auth/
    login/
      ...
    validate_token/
      ...

TypeScript equivalent:

src/
  domains/
    billing/
      calculate-overage/
        index.ts                ← public interface
        handler.ts              ← route handler
        service.ts              ← business logic
        repository.ts           ← data access
        types.ts                ← request/response types
        service.test.ts         ← unit tests
        handler.test.ts         ← integration tests
      send-invoice/
        ...
    auth/
      login/
        ...

The Slice Contract

Each slice exposes a public interface through its __init__.py (Python) or index.ts (TypeScript). External code imports only from the public interface — never from internal slice files directly.

# domains/billing/calculate_overage/__init__.py
# Public interface — the only import point for external code

from .handler import calculate_overage_handler
from .types import CalculateOverageRequest, CalculateOverageResponse

__all__ = [
    "calculate_overage_handler",
    "CalculateOverageRequest",
    "CalculateOverageResponse",
]

# What is NOT exported: service, repository, internal helpers
# External code cannot depend on internal implementation details
// src/domains/billing/calculate-overage/index.ts
// Public interface — the only import point for external code

export { calculateOverageHandler } from './handler'
export type { CalculateOverageRequest, CalculateOverageResponse } from './types'

// Service and repository are NOT exported
// External code cannot depend on internal implementation details

This contract has a structural consequence: when a slice's internal implementation changes — the service is refactored, the repository query is optimized, the types are renamed — the change is invisible to external code as long as the public interface remains stable. The blast radius of any internal change is bounded by the slice boundary.


The Duplication Principle

Slice isolation applies a deliberate duplication principle: within a slice, duplicate rather than share. If two slices need similar logic — email validation, date formatting, currency rounding — each slice implements its own version rather than importing from a shared utility.

This is counterintuitive. The conventional instinct is to extract shared logic immediately. The slice isolation principle defers extraction until the duplication is proven necessary:

# ✅ Correct: each slice has its own validation
# domains/billing/calculate_overage/service.py
def _validate_amount(amount: Decimal) -> None:
    if amount < 0:
        raise ValueError("Amount cannot be negative")

# domains/billing/send_invoice/service.py
def _validate_amount(amount: Decimal) -> None:
    if amount < 0:
        raise ValueError("Amount cannot be negative")

# ❌ Premature: shared utility extracted after first duplication
# domains/shared/validation.py
# def validate_amount(amount: Decimal) -> None: ...
# Now both slices depend on shared/validation — coupling introduced

The extraction threshold: extract to a shared utility only when three or more slices need the same logic, and the logic is stable enough that changes to it should propagate to all consumers simultaneously. Below that threshold, duplication is the correct choice.


Slice Size Enforcement

Each slice has a hard size ceiling. A slice that exceeds the ceiling is a signal that the operation has accumulated logic from multiple concerns and should be split.

# Check slice sizes — flag any slice exceeding the ceiling
echo "=== Slice size check ==="
find . -path "*/domains/*/*" -name "service.py" -o \
       -path "*/domains/*/*" -name "service.ts" 2>/dev/null | \
  while read f; do
    lines=$(wc -l < "$f" 2>/dev/null)
    slice=$(dirname "$f")
    if [ "$lines" -gt 200 ]; then
      echo "OVERSIZED SLICE: $slice/service ($lines LOC) — consider splitting"
    fi
  done

# Count operations per domain (domain size check)
echo ""
echo "=== Operations per domain ==="
find . -path "*/domains/*" -mindepth 2 -maxdepth 2 -type d 2>/dev/null | \
  awk -F/ '{print $(NF-1)}' | sort | uniq -c | sort -rn | head -10

Slice size thresholds:

File Warning Critical
service.py / service.ts >150 LOC >200 LOC
handler.py / handler.ts >80 LOC >120 LOC
repository.py / repository.ts >100 LOC >150 LOC
types.py / types.ts >60 LOC >100 LOC

A service file exceeding 200 LOC is a signal that the slice contains more than one operation. The correct response is to split the slice into two operations, each with its own directory.


Migrating from Type-Based to Slice-Based Organization

Migrating an existing codebase from type-based organization (services/, repositories/, handlers/) to slice-based organization is a structural refactoring that does not require changing business logic.

Before (type-based):
  services/
    billing.py          (847 LOC — multiple operations)
    auth.py             (623 LOC — multiple operations)
  repositories/
    billing.py
    auth.py
  handlers/
    billing.py
    auth.py

After (slice-based):
  domains/
    billing/
      calculate_overage/    (service: 95 LOC)
      send_invoice/         (service: 112 LOC)
      apply_coupon/         (service: 88 LOC)
    auth/
      login/                (service: 78 LOC)
      validate_token/       (service: 67 LOC)
      refresh_session/      (service: 71 LOC)

Migration sequence:

Step 1 (1–2 hours): Map the operations
  → List every discrete operation in each large service file
  → Group by domain (billing, auth, user, notifications)
  → Identify the public interface each operation exposes

Step 2 (per operation, 30–60 min each): Extract to slice
  → Create the slice directory
  → Move the operation's handler, service, and repository code
  → Create the __init__.py / index.ts public interface
  → Update imports in all files that referenced the old location
  → Run tests to verify behavior is preserved

Step 3 (1–2 hours): Configure boundary enforcement
  → Add dependency-cruiser rules to prevent cross-slice imports
    (slices must communicate through public interfaces only)
  → Add slice size check to CI/CD pre-commit hook

Step 4 (ongoing): Enforce the slice contract
  → New operations always get their own slice directory
  → No new code is added to existing slices unless it belongs to that operation
  → Extraction to shared utilities requires explicit decision (3+ consumers)

How Slice Isolation Interacts with Other Techniques

Slice isolation is the organizational foundation that makes the other structural techniques more effective:

  • Boundary enforcement — slice boundaries are the natural enforcement unit; the dependency-cruiser rules enforce that external code imports only from slice public interfaces
  • Production safety layer — isolated slices are independently testable; the test baseline can be established one slice at a time, starting with the highest-risk operations
  • CI/CD safety pipeline — slice size checks run as a pre-commit hook; a slice that exceeds the size ceiling fails the commit before it is reviewed