Skip to main content
Clean Architecture 70 XP · 7 min

Policy and Level

Level is the distance from inputs and outputs — the farther from I/O, the higher the level. Source code dependencies should always point toward the highest-level, most stable policies.

Showing
Ad (728×90)

Why this matters

Every policy in a system has a level — defined by its distance from inputs and outputs. Reading from a file is low-level: it's directly adjacent to an input device. Writing to a display is low-level: it's adjacent to an output device. An encryption algorithm, however, is high-level: it transforms data but doesn't care where data came from or where it will go. It is maximally distant from any particular I/O device.

This concept governs dependency direction. Low-level policies change often — file formats, API shapes, UI structures. High-level policies change rarely — business logic, algorithms, fundamental rules. If a low-level policy depends on a high-level one (correctly), a change to the file format never touches the encryption logic. If the dependency is reversed — if the high-level algorithm knows about the file — a trivial change to input format requires touching the most valuable code in the system.

The problem

An encrypt() function that simultaneously opens files, iterates characters, and applies the cipher — all levels mixed in one function, impossible to reuse the cipher logic alone.

Bad

import sys

def encrypt(input_path: str, output_path: str, shift: int) -> None:
    # Low-level: file I/O (close to input/output devices)
    with open(input_path, "r") as fin, open(output_path, "w") as fout:
        for line in fin:
            result = ""
            for char in line:
                # High-level: cipher rule (farthest from I/O)
                if char.isalpha():
                    base = ord("A") if char.isupper() else ord("a")
                    result += chr((ord(char) - base + shift) % 26 + base)
                else:
                    result += char
            fout.write(result)

# The cipher algorithm is buried inside file-open and file-write.
# To reuse the cipher for HTTP, stdin, or sockets: copy-paste only.
# To test the cipher alone: must provide real files.
# All levels are at the same level — there is no level.
import { readFileSync, writeFileSync } from "fs";

export function encryptFile(inputPath: string, outputPath: string, shift: number): void {
  const content = readFileSync(inputPath, "utf-8");  // low-level

  const result = content.split("").map(char => {     // mixed levels
    if (/[a-z]/i.test(char)) {
      const base   = char >= "a" ? 97 : 65;
      const offset = (char.charCodeAt(0) - base + shift) % 26;
      return String.fromCharCode(base + offset);     // high-level cipher rule
    }
    return char;
  }).join("");

  writeFileSync(outputPath, result, "utf-8");        // low-level
}
// The cipher is trapped inside file I/O. Not reusable. Not independently testable.

The solution

Pure cipher function at the highest level — knows nothing about I/O. File and stdin wrappers at lower levels depend on it, not the reverse.

Good

# High-level policy (farthest from I/O) — pure and reusable
def caesar_cipher(text: str, shift: int) -> str:
    result = []
    for char in text:
        if char.isalpha():
            base = ord("A") if char.isupper() else ord("a")
            result.append(chr((ord(char) - base + shift) % 26 + base))
        else:
            result.append(char)
    return "".join(result)

# Lower-level: file-based wiring (depends on caesar_cipher, not vice versa)
def encrypt_file(input_path: str, output_path: str, shift: int) -> None:
    with open(input_path, "r") as fin, open(output_path, "w") as fout:
        for line in fin:
            fout.write(caesar_cipher(line, shift))

# Lower-level: stdin/stdout wiring (depends on the same high-level rule)
def encrypt_stdin(shift: int) -> None:
    import sys
    for line in sys.stdin:
        sys.stdout.write(caesar_cipher(line, shift))

# Dependency direction: encrypt_file → caesar_cipher (low → high, correct)
# Test: assert caesar_cipher("Hello", 1) == "Ifmmp"  — no file system needed.
// High-level policy — depends on nothing, changes only for business reasons
export function caesarCipher(text: string, shift: number): string {
  return text.split("").map(char => {
    if (/[a-z]/i.test(char)) {
      const base   = char >= "a" ? 97 : 65;
      const offset = (char.charCodeAt(0) - base + shift) % 26;
      return String.fromCharCode(base + offset);
    }
    return char;
  }).join("");
}

// Lower-level: file-based wiring
import { readFileSync, writeFileSync } from "fs";
export function encryptFile(inputPath: string, outputPath: string, shift: number): void {
  writeFileSync(outputPath, caesarCipher(readFileSync(inputPath, "utf-8"), shift), "utf-8");
}

// Lower-level: HTTP wiring (same high-level rule, different delivery mechanism)
import type { Request, Response } from "express";
export function encryptEndpoint(req: Request, res: Response): void {
  const { text, shift } = req.body as { text: string; shift: number };
  res.json({ encrypted: caesarCipher(text, shift) });
}

// caesarCipher depends on nothing.
// encryptFile and encryptEndpoint depend on caesarCipher.
// Dependencies flow from volatile (I/O) → stable (cipher rule).
// Test: expect(caesarCipher("Hello", 1)).toBe("Ifmmp");

Key takeaway

High-level policies change rarely and for business reasons. Low-level details change often and for technical reasons. Dependencies should flow from volatile to stable — from the I/O boundary inward toward the algorithm. Never the reverse.

Done with this lesson?

Mark it complete to earn XP and track your progress.