Claude
Skills
Sign in
Back

orthogonality-principle

Included with Lifetime
$97 forever

Use when designing modules, APIs, and system architecture requiring independent, non-overlapping components where changes in one don't affect others.

General

What this skill does


# Orthogonality Principle

Build systems where components are independent and changes don't ripple
unexpectedly.

## What is Orthogonality?

**Orthogonal** (from mathematics): Two lines are orthogonal if they're
at right angles - changing one doesn't affect the other.

**In software**: Components are orthogonal when changing one doesn't
require changing others. They are independent and non-overlapping.

### Benefits

- Changes are localized (less debugging)
- Easy to test in isolation
- Components are reusable
- Less coupling = less complexity
- Easier to understand and maintain

## Signs of Non-Orthogonality

### Red flags indicating components are NOT orthogonal

1. **Change amplification**: Changing one thing requires changing many others
2. **Shotgun surgery**: One feature scattered across many files
3. **Tight coupling**: Components know too much about each other
4. **Duplicate logic**: Same concept implemented multiple ways
5. **Cascading changes**: Change in A breaks B, C, D unexpectedly

## Achieving Orthogonality

### 1. Separation of Concerns

### Keep unrelated responsibilities separate

### Elixir Example

```elixir
# NON-ORTHOGONAL - Mixed concerns
defmodule UserController do
  def create(conn, params) do
    # Validation
    if valid_email?(params["email"]) do
      # Database
      user = Repo.insert!(%User{email: params["email"]})

      # External API
      Stripe.create_customer(user.email)

      # Notification
      Email.send_welcome(user.email)

      # Logging
      Logger.info("Created user #{user.id}")

      # Response
      json(conn, %{user: user})
    end
  end
end
# Changing email format affects validation, database, Stripe, email!

# ORTHOGONAL - Separated concerns
defmodule UserController do
  def create(conn, params) do
    with {:ok, command} <- build_command(params),
         {:ok, user} <- UserService.create(command) do
      json(conn, %{user: user})
    end
  end
end

defmodule UserService do
  def create(command) do
    with {:ok, user} <- Repo.insert(User.changeset(command)),
         :ok <- BillingService.setup_customer(user),
         :ok <- NotificationService.welcome(user) do
      {:ok, user}
    end
  end
end

# Now can change billing without touching notifications
# Can change notifications without touching database
# Each service is orthogonal
```

### TypeScript Example

```typescript
// NON-ORTHOGONAL - Everything in one component
function TaskList() {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [filters, setFilters] = useState<Filters>({});
  const [sorting, setSorting] = useState<Sort>({ field: 'date', dir: 'asc' });

  // Data fetching
  useEffect(() => {
    fetch('/api/tasks').then(res => res.json()).then(setTasks);
  }, []);

  // Filtering logic
  const filtered = tasks.filter(gig => {
    if (filters.status && gig.status !== filters.status) return false;
    if (filters.location && !gig.location.includes(filters.location)) return false;
    return true;
  });

  // Sorting logic
  const sorted = [...filtered].sort((a, b) => {
    const aVal = a[sorting.field];
    const bVal = b[sorting.field];
    return sorting.dir === 'asc' ? aVal - bVal : bVal - aVal;
  });

  // Rendering
  return (
    <View>
      {/* Filters UI */}
      {/* Sorting UI */}
      {/* List UI */}
    </View>
  );
}
// Changing filtering affects fetching, sorting, rendering!

// ORTHOGONAL - Separated concerns
function useTaskData() {
  const [tasks, setTasks] = useState<Task[]>([]);
  useEffect(() => {
    fetch('/api/tasks').then(res => res.json()).then(setTasks);
  }, []);
  return tasks;
}

function useTaskFiltering(tasks: Task[], filters: Filters) {
  return useMemo(() => {
    return tasks.filter(gig => {
      if (filters.status && gig.status !== filters.status) return false;
      if (filters.location && !gig.location.includes(filters.location)) return false;
      return true;
    });
  }, [tasks, filters]);
}

function useTaskSorting(tasks: Task[], sort: Sort) {
  return useMemo(() => {
    return [...tasks].sort((a, b) => {
      const aVal = a[sort.field];
      const bVal = b[sort.field];
      return sort.dir === 'asc' ? aVal - bVal : bVal - aVal;
    });
  }, [tasks, sort]);
}

function TaskList() {
  const allTasks = useTaskData();
  const [filters, setFilters] = useState<Filters>({});
  const [sort, setSort] = useState<Sort>({ field: 'date', dir: 'asc' });

  const filtered = useTaskFiltering(allTasks, filters);
  const sorted = useTaskSorting(filtered, sort);

  return (
    <View>
      <TaskFilters filters={filters} onChange={setFilters} />
      <TaskSorting sort={sort} onChange={setSort} />
      <TaskCards tasks={sorted} />
    </View>
  );
}
// Now can change filtering without touching sorting
// Can change data fetching without touching UI
// Each concern is orthogonal
```

### 2. Interface Segregation

### Create focused, minimal interfaces

### Elixir Example (Interface Segregation)

```elixir
# NON-ORTHOGONAL - Fat interface
defmodule DataStore do
  @callback get(key :: String.t()) :: {:ok, term()} | {:error, term()}
  @callback set(key :: String.t(), value :: term()) :: :ok
  @callback delete(key :: String.t()) :: :ok
  @callback list_all() :: [term()]
  @callback search(query :: String.t()) :: [term()]
  @callback bulk_insert(items :: [term()]) :: :ok
  @callback export_to_json() :: String.t()
  @callback import_from_json(json :: String.t()) :: :ok
end
# Implementing simple cache requires implementing export/import!
# Not orthogonal - simple use cases coupled to complex ones

# ORTHOGONAL - Segregated interfaces
defmodule KeyValueStore do
  @callback get(key :: String.t()) :: {:ok, term()} | {:error, term()}
  @callback set(key :: String.t(), value :: term()) :: :ok
  @callback delete(key :: String.t()) :: :ok
end

defmodule Searchable do
  @callback search(query :: String.t()) :: [term()]
end

defmodule BulkOperations do
  @callback bulk_insert(items :: [term()]) :: :ok
end

defmodule Exportable do
  @callback export_to_json() :: String.t()
  @callback import_from_json(json :: String.t()) :: :ok
end

# Simple cache implements only KeyValueStore
# Search index implements KeyValueStore + Searchable
# Each interface is orthogonal to others
```

### 3. Dependency Injection

### Don't hardcode dependencies - inject them

### Elixir Example (Dependency Injection)

```elixir
# NON-ORTHOGONAL - Hardcoded dependencies
defmodule OrderService do
  def create_order(items) do
    PaymentService.charge(items)  # Coupled
    InventoryService.reserve(items)  # Coupled
    EmailService.send_confirmation()  # Coupled
  end
end
# Can't test without real payment/inventory/email services
# Can't swap implementations

# ORTHOGONAL - Injected dependencies
defmodule OrderService do
  def create_order(items, deps \\ default_deps()) do
    with :ok <- deps.payment.charge(items),
         :ok <- deps.inventory.reserve(items),
         :ok <- deps.email.send_confirmation() do
      :ok
    end
  end

  defp default_deps do
    %{
      payment: PaymentService,
      inventory: InventoryService,
      email: EmailService
    }
  end
end

# Can test with mocks
test "creates order" do
  deps = %{
    payment: MockPayment,
    inventory: MockInventory,
    email: MockEmail
  }
  assert :ok = OrderService.create_order(items, deps)
end
# Each dependency is orthogonal - can change independently
```

### 4. Event-Driven Architecture

### Decouple through events instead of direct calls

### Elixir Example (Event-Driven Architecture)

```elixir
# NON-ORTHOGONAL - Direct coupling
defmodule UserService do
  def create_user(attrs) do
    {:ok, user} = Repo.insert(User.changeset(attrs))

    # Directly coupled to all these services
    BillingService.create_customer(user)
    AnalyticsService.track_signup(user)
    EmailService.send_welcome(user)
    CacheService.invalidate("users")

    {:ok, user}
  end
end
# Adding new behavior requires modifying UserService
# Removing email feature requires modifying UserService

Related in General