From core
Guides applying orthogonality principle to design independent modules, APIs, and system architectures, localizing changes and reducing coupling.
How this skill is triggered — by the user, by Claude, or both
Slash command
/core:orthogonality-principleThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Build systems where components are independent and changes don't ripple
Build systems where components are independent and changes don't ripple unexpectedly.
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.
# 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
// 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
# 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
# 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
# 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
# ORTHOGONAL - Event-driven
defmodule UserService do
def create_user(attrs) do
{:ok, user} = Repo.insert(User.changeset(attrs))
# Publish event - don't know who listens
EventBus.publish({:user_created, user})
{:ok, user}
end
end
# Subscribers are orthogonal
defmodule BillingSubscriber do
def handle_event({:user_created, user}) do
BillingService.create_customer(user)
end
end
defmodule AnalyticsSubscriber do
def handle_event({:user_created, user}) do
AnalyticsService.track_signup(user)
end
end
# Add/remove subscribers without touching UserService
# Each subscriber is orthogonal to others
// NON-ORTHOGONAL - Direct coupling
class TaskManager {
createTask(data: TaskData) {
const gig = this.repository.save(data);
// Directly coupled
this.notificationService.notifyUsersNearby(gig);
this.searchIndex.addTask(gig);
this.analyticsService.trackTaskCreated(gig);
return gig;
}
}
// ORTHOGONAL - Event-driven
class TaskManager {
constructor(
private repository: TaskRepository,
private eventBus: EventBus
) {}
createTask(data: TaskData) {
const gig = this.repository.save(data);
// Publish event
this.eventBus.publish('gig.created', gig);
return gig;
}
}
// Orthogonal subscribers
eventBus.subscribe('gig.created', (gig) => {
notificationService.notifyUsersNearby(gig);
});
eventBus.subscribe('gig.created', (gig) => {
searchIndex.addTask(gig);
});
// Add/remove subscribers without changing TaskManager
# NON-ORTHOGONAL - Duplicate data
defmodule Task do
schema "tasks" do
field :hourly_rate, :decimal
field :total_hours, :integer
field :total_amount, :decimal # Calculated from rate * hours
# Changing hourly_rate requires updating total_amount
end
end
# ORTHOGONAL - Computed fields
defmodule Task do
schema "tasks" do
field :hourly_rate, :decimal
field :total_hours, :integer
# total_amount computed on demand
end
def total_amount(%{hourly_rate: rate, total_hours: hours}) do
Decimal.mult(rate, hours)
end
end
# Single source of truth - rate and hours
# total_amount always correct, no sync issues
// NON-ORTHOGONAL - Duplicate state
interface Assignment {
status: 'pending' | 'active' | 'completed';
isPending: boolean; // Duplicates status
isActive: boolean; // Duplicates status
isCompleted: boolean; // Duplicates status
}
// Have to keep all flags in sync with status
// ORTHOGONAL - Single source of truth
interface Assignment {
status: 'pending' | 'active' | 'completed';
}
// Derive flags from status
function isPending(engagement: Assignment): boolean {
return engagement.status === 'pending';
}
function isActive(engagement: Assignment): boolean {
return engagement.status === 'active';
}
// One source of truth, no sync issues
Good test: Tests one component without needing to set up unrelated components
# ORTHOGONAL - Test in isolation
test "calculates gig total" do
gig = %Task{hourly_rate: Decimal.new(25), total_hours: 8}
assert Task.total_amount(gig) == Decimal.new(200)
end
# No database, no external services, pure logic
# NON-ORTHOGONAL - Requires full setup
test "calculates gig total" do
{:ok, requester} = create_requester()
{:ok, worker} = create_worker()
{:ok, gig} = create_gig(requester)
{:ok, engagement} = create_engagement(gig, worker)
{:ok, shift} = create_shift(engagement, hours: 8)
assert calculate_total(shift) == Decimal.new(200)
end
# Have to set up requester, worker, gig, engagement just to test math
CQRS: Commands/Queries are orthogonal
Atomic Design: Atoms/Molecules/Organisms are orthogonal
GraphQL Schema: Types are orthogonal
Microservices: Bounded contexts are orthogonal
solid-principles: Single Responsibility → Orthogonalitystructural-design-principles: Encapsulation → Orthogonalitysimplicity-principles: KISS → Fewer dependencies → More orthogonalcqrs-pattern: Commands/Queries naturally orthogonalatomic-design-pattern: Component hierarchy naturally orthogonal"Orthogonal systems are easier to design, build, test, and extend."
The more orthogonal your system, the more flexible and maintainable it becomes.
npx claudepluginhub thebushidocollective/han --plugin coreGuides module and component design using paradigm-agnostic principles: Composition Over Inheritance, Law of Demeter, Tell Don't Ask, Encapsulation. Examples in Elixir and TypeScript/React.
Guides domain-driven design and hexagonal architecture with functional core pattern. Use when designing features, modeling domains, breaking down tasks, or evaluating component responsibilities.
Applies Python design principles like KISS, SRP, composition over inheritance, and Rule of Three for designing services, refactoring monoliths, reducing coupling, and improving testability.