Why this matters
In the 1960s, programs were written directly for punched cards. When magnetic tape appeared, code had to be completely rewritten — not because the business logic changed, but because the input device changed. Device-independent operating systems solved this by abstracting I/O into a uniform interface: the same program ran on cards, tape, or disk without modification. The same lesson applies today.
HTTP, stdin, message queues, and file uploads are all delivery mechanisms — they are the modern equivalent of punched cards and tape. Your business logic should be as indifferent to how data arrives as an OS is to which physical disk is installed. When payroll logic knows about sys.stdin, it's a program written for punched cards all over again.
The problem
A payroll function that reads from stdin and writes to a printer device — the business rule is buried inside device-specific code and cannot be reused or tested alone.
Bad
import sys
def run_payroll():
for line in sys.stdin: # tied to stdin
parts = line.strip().split(",")
employee_id = parts[0]
hours, rate = float(parts[1]), float(parts[2])
gross = hours * rate
tax = gross * 0.2
net = gross - tax
with open("/dev/lp0", "w") as printer: # tied to printer device
printer.write(f"Employee {employee_id}: net={net:.2f}\n")
# Switching to REST API input: rewrite run_payroll entirely.
# Switching output from printer to PDF: rewrite run_payroll entirely.
# Testing: must pipe data through stdin and capture printer output.
import { Request, Response } from "express";
export function processPayroll(req: Request, res: Response): void {
const employees = req.body.employees as Array<{ id: string; hours: number; rate: number }>;
const results = employees.map(emp => {
const gross = emp.hours * emp.rate;
const tax = gross * 0.2;
const net = gross - tax;
return `<tr><td>${emp.id}</td><td>${net.toFixed(2)}</td></tr>`;
});
res.send(`<table>${results.join("")}</table>`);
}
// Business rule (tax calculation) is trapped inside an HTTP handler.
// Can't reuse for CLI payroll runs, PDF reports, or batch jobs.
The solution
Pure calculation function with no I/O — input source and output destination are wired at the boundary, not inside the business rule.
Good
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class Employee:
id: str
hours: Decimal
rate: Decimal
@dataclass
class PaySlip:
employee_id: str
gross: Decimal
tax: Decimal
net: Decimal
def calculate_payroll(employees: list[Employee]) -> list[PaySlip]:
"""Pure function — no stdin, no files, no HTTP. Device-agnostic."""
slips = []
for emp in employees:
gross = emp.hours * emp.rate
tax = gross * Decimal("0.2")
net = gross - tax
slips.append(PaySlip(emp.id, gross, tax, net))
return slips
# Wired at the boundary — not inside the rule:
# stdin: employees = parse_stdin_csv(sys.stdin)
# REST API: employees = parse_request_body(request.json)
# The same calculate_payroll() call in all cases.
export interface Employee { id: string; hours: number; rate: number; }
export interface PaySlip { employeeId: string; gross: number; tax: number; net: number; }
export function calculatePayroll(employees: Employee[]): PaySlip[] {
return employees.map(emp => {
const gross = emp.hours * emp.rate;
const tax = gross * 0.2;
const net = gross - tax;
return { employeeId: emp.id, gross, tax, net };
});
}
// HTTP boundary wires it for REST:
// app.post("/payroll", (req, res) => {
// const slips = calculatePayroll(req.body.employees);
// res.json(slips);
// });
// CLI boundary wires it for stdin:
// const employees = parseStdinCsv(process.stdin);
// const slips = calculatePayroll(employees);
// printToConsole(slips);
// calculatePayroll is testable with plain arrays — no I/O mocking needed.
Key takeaway
If your business logic knows what HTTP is, it's coupled to a delivery mechanism it shouldn't care about. HTTP, stdin, message queues, and files are all equivalent punched-card readers — wire them at the boundary and keep the business rule device-agnostic.