Skip to main content
Clean Architecture 80 XP · 8 min

The Main Sequence: The A/I Balance

Components should balance Abstraction (A) and Instability (I) — the Pain Zone (stable+concrete) and Uselessness Zone (abstract+unstable) are architectural anti-patterns to avoid.

Showing
Ad (728×90)

Why this matters

The Main Sequence is the ideal line on a two-dimensional graph where the axes are Abstraction (A) and Instability (I), both from 0 to 1. The Main Sequence is the line A + I = 1. Components on this line are perfectly balanced: either stable and abstract, or volatile and concrete.

Pain Zone (A≈0, I≈0): Stable and concrete. A component that many modules depend on, but that contains only concrete implementation. It cannot be extended without modification. Every new requirement requires editing it, which ripples to all dependents. This is the most dangerous zone — the component resists change but cannot evolve. Think of a god class or a catch-all utility module.

Uselessness Zone (A≈1, I≈1): Abstract and unstable. A component of pure interfaces that nobody depends on. It was perhaps created in anticipation of future use, or it was heavily depended-upon once but all those dependents were refactored away. It is abstract but vestigial — present without purpose.

Distance from Main Sequence: D = |A + I − 1|. Ideal D = 0. Any component with D > 0.3 is worth investigating. You can measure this automatically with static analysis tools and include D in CI quality gates.

The problem

A DatabaseManager class highly depended upon (I≈0) but entirely concrete (A≈0). D = 1.0 — maximum distance from the Main Sequence. Every new requirement forces editing this stable component.

✗ Bad — Pain Zone (A≈0, I≈0)

# database_manager.py  ← imported by 15 modules (stable, I≈0)
import sqlite3

class DatabaseManager:
    def __init__(self, db_path: str):
        self._conn = sqlite3.connect(db_path)  # concrete: SQLite

    def get_user(self, user_id: str) -> dict:
        cursor = self._conn.execute(
            "SELECT * FROM users WHERE id = ?", (user_id,)
        )
        return dict(cursor.fetchone())

    def save_order(self, order: dict) -> None:
        self._conn.execute(
            "INSERT INTO orders (id, total) VALUES (?, ?)",
            (order["id"], order["total"])
        )

# D = |0 + 0 - 1| = 1.0  ← maximum distance from Main Sequence
# Adding PostgreSQL support: rewrite everything. Pain Zone confirmed.
// DatabaseManager.ts  ← imported by 15 modules (stable, I≈0)
import { Pool } from "pg";

export class DatabaseManager {
  private pool = new Pool({ connectionString: process.env.DATABASE_URL });

  async getUser(userId: string): Promise> {
    const result = await this.pool.query(
      "SELECT * FROM users WHERE id = $1", [userId]
    );
    return result.rows[0];
  }

  async saveOrder(order: { id: string; total: number }): Promise {
    await this.pool.query(
      "INSERT INTO orders (id, total) VALUES ($1, $2)", [order.id, order.total]
    );
  }
}

// D = |0 + 0 - 1| = 1.0  ← maximum distance from Main Sequence
// Adding MySQL support: rewrite everything. Pain Zone confirmed.

The solution

Split into DatabasePort (stable, abstract, D=0) and PostgresDatabaseAdapter (volatile, concrete, D=0). Each sits exactly on the Main Sequence.

✓ Good — On the Main Sequence

# database_port.py  ← stable (I≈0), abstract (A≈1)
from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class UserRecord:
    id: str
    email: str

@dataclass
class OrderRecord:
    id: str
    total: float

class DatabasePort(ABC):
    @abstractmethod
    def get_user(self, user_id: str) -> UserRecord: ...

    @abstractmethod
    def save_order(self, order: OrderRecord) -> None: ...

# D(DatabasePort) = |1 + 0 - 1| = 0  ← on the Main Sequence

# postgres_adapter.py  ← volatile (I≈1), concrete (A≈0)
import psycopg2

class PostgresDatabaseAdapter(DatabasePort):
    def __init__(self, dsn: str):
        self._conn = psycopg2.connect(dsn)

    def get_user(self, user_id: str) -> UserRecord:
        cur = self._conn.cursor()
        cur.execute("SELECT id, email FROM users WHERE id = %s", (user_id,))
        row = cur.fetchone()
        return UserRecord(id=row[0], email=row[1])

    def save_order(self, order: OrderRecord) -> None:
        cur = self._conn.cursor()
        cur.execute("INSERT INTO orders VALUES (%s, %s)", (order.id, order.total))
        self._conn.commit()

# D(PostgresDatabaseAdapter) = |0 + 1 - 1| = 0  ← on the Main Sequence
// DatabasePort.ts  ← stable (I≈0), abstract (A≈1)
export interface UserRecord  { id: string; email: string; }
export interface OrderRecord { id: string; total: number; }

export interface DatabasePort {
  getUser(userId: string): Promise;
  saveOrder(order: OrderRecord): Promise;
}

// D(DatabasePort) = |1 + 0 - 1| = 0  ← on the Main Sequence

// PostgresDatabaseAdapter.ts  ← volatile (I≈1), concrete (A≈0)
import { Pool } from "pg";
import { DatabasePort, UserRecord, OrderRecord } from "./DatabasePort";

export class PostgresDatabaseAdapter implements DatabasePort {
  private pool = new Pool({ connectionString: process.env.DATABASE_URL });

  async getUser(userId: string): Promise {
    const result = await this.pool.query(
      "SELECT id, email FROM users WHERE id = $1", [userId]
    );
    return result.rows[0] as UserRecord;
  }

  async saveOrder(order: OrderRecord): Promise {
    await this.pool.query(
      "INSERT INTO orders (id, total) VALUES ($1, $2)", [order.id, order.total]
    );
  }
}

// D(PostgresDatabaseAdapter) = |0 + 1 - 1| = 0  ← on the Main Sequence

Key takeaway

Calculate D = |A + I − 1| for your key components. Anything above 0.3 is a design smell worth investigating. Components in the Pain Zone (stable+concrete) need interfaces extracted from them. Components in the Uselessness Zone (abstract+unstable) need either new dependents or deletion. The Main Sequence is the architectural optimum — balance abstraction with stability.

Done with this lesson?

Mark it complete to earn XP and track your progress.