❖ SOLID, Clean Architecture & Unix philosophy

As software systems grow more complex, it's easy for code to become tangled, with classes that do too much, logic that's hard to reuse, and changes in one place breaking unrelated parts of the app. To combat this, developers lean on design principles, guidelines that help us write clean, modular, and maintainable code. One of the most influential sets of principles in modern software engineering is known as 'SOLID'. SOLID outlines five key rules for object-oriented design. But even beyond OOP, these principles help teams write better code , whether you're building a mobile app, scripting in shell, or designing a backend service.

Understanding SOLID means understanding how to break software into well-defined, independent parts — so each part is easier to change, extend, and reason about.

This article contains 3 separate parts:

- Does Unix follow SOLID principles?

- When does a system become large enough to require separation of responsibilities?

- When does it start making sense to introduce Clean Architecture into a project?


Does Unix follow SOLID principles?

Not literally, the SOLID principles were coined for object-oriented programming by Robert C. Martin (Uncle Bob). Unix, on the other hand, is rooted in process-based design, with utilities built primarily in C and shell scripts, not classes and objects.

But philosophically, Unix aligns with many of the same ideas, especially the most important one: Single Responsibility Principle (SRP).

What is SOLID? Why it Matters

SOLID is an acronym for five design principles that aim to make software easier to understand, maintain, and extend.

Principle Description
Single Responsibility A class should have only one reason to change
Open/Closed Software should be open for extension, but closed for modification
Liskov Substitution Subtypes must be substitutable for their base types
Interface Segregation Prefer many small, specific interfaces over large, general ones
Dependency Inversion Depend on abstractions, not concrete implementations

These principles are key in OOP and Clean Architecture — but they also echo Unix's composable, minimalist philosophy.


S - Single Responsibility Principle

In OOP:

A class should have one, and only one, reason to change.

It means every class (or function, or module) should do one thing well. If it has multiple responsibilities, it will be harder to maintain and more prone to bugs.

In Unix:

Each command-line utility is designed to do one job and do it well.

Examples:

These tools are intentionally small and composable. You combine them using pipes (|) to build complex behaviors.

Example:

cat access.log | grep 404 | cut -d' ' -f1 | sort | uniq -c | sort -nr

Each tool:

That’s SRP — just not in class-based code, but in process-based design.


Other SOLID principles in Unix

Principle Unix Alignment
Single Responsibility ✅ Strong: each tool has a narrow purpose
Open/Closed 🟡 Somewhat: tools are extended through scripting, not internal modification
Liskov Substitution ❌ Not really applicable — inheritance isn’t used
Interface Segregation ✅ Strong: tools use minimal, focused inputs/outputs (text streams, args)
Dependency Inversion 🟡 Weak: tools call each other via scripts but lack abstraction mechanisms
Unix doesn't enforce these principles through classes — but its *culture* encourages similar separation of concerns.

SRP in Android App Architecture

SRP isn’t just for classes. In an Android app (especially with Clean Architecture), SRP applies to:

Layers

Components

If a component does too many things — UI + business logic + data access — it becomes hard to test, understand, or change.

SRP Examples

Component Responsibility
LoginViewModel Manage login UI state and events
LoginUseCase Handle login logic and validation
UserRepo Fetch and cache user data (from DB or API)

Analogy: SRP in Unix vs Android

Aspect Unix Utility Android Component
One Concern cut extracts fields LoginUseCase handles login only
Composability Combine via pipes (|) Combine via layers (domain/UI)
Reason to Change grep only changes if filtering changes UserRepo changes only if data layer changes

Visual Diagram: Unix Shell Pipeline vs Android Architecture

Here’s a conceptual side-by-side comparison:

Unix Shell Pipeline Android Architecture Flow
cat file.txt Repository provides raw data
grep error UseCase filters meaningful business data
cut -d' ' -f1 ViewModel extracts/display-relevant state
sort | uniq -c UI Layer formats and displays info

Both systems:


Interpreting Open/Closed and Dependency Inversion in Unix

Open/Closed Principle (OCP) in Unix

"Software entities should be open for extension but closed for modification."

How Unix achieves this:

Example:

mygrep() {
  grep --color=auto "$@" | less
}

The grep command wasn’t modified — it was extended.

Dependency Inversion Principle (DIP) in Unix

"High-level modules should not depend on low-level modules. Both should depend on abstractions."

Unix tools don't have formal abstractions (like interfaces or abstract classes), but:

Example:

if [ -x ./mybackend ]; then
  ./mybackend "$@"
else
  default_backend "$@"
fi

This allows swappable implementations — an inversion of control.


Summary



When does a system become large enough to require separation of responsibilities?

This is one of the hardest and most important questions in applying the Single Responsibility Principle (SRP) well. There’s no universal line, it’s a judgment call, but there are practical guidelines and heuristics to help you decide when a piece of code needs to be split due to too many responsibilities.

Don’t split prematurely, SRP is about clarity and safety, not bureaucracy.

Understanding Responsibility

A responsibility is a reason to change, not just a “thing it does.”

**Robert C. Martin (Uncle Bob):**“A responsibility is a reason to change.”

So the key test is: If a change request comes in, which part of this component will it affect?


🧭 Guiding Principles to Decide When to Split

1. ❗ You’re Changing for More Than One Reason

Signal: If two or more distinct types of changes would require touching the same file/class/module.

Examples:

2. 🧪 It's Hard to Unit Test

If you're struggling to write a small, focused unit test, the class probably does too much.

Heuristic:

3. It Uses Unrelated Domains

Signal: The class uses concepts from more than one domain.

Android example:

snippet.kotlin
class UserProfileManager {
    fun drawUI() { ... }
    fun fetchFromServer() { ... }
    fun saveToDatabase() { ... }
}

This class:

These are 3 separate domains → split it.

4. 🔁 You Keep Coming Back to It for Unrelated Fixes

When one class keeps being involved in a wide variety of issues or changes — it's a red flag.

5. Cognitive Load (Lines of Code is a symptom, not a cause)

There’s no magic LOC number, but if:

…it likely violates SRP.


Scaling Thresholds for Separation

Project Size SRP Enforcement Strategy
Small scripts / proof-of-concepts Don't over-engineer. A bit of mess is fine.
Single-developer apps Start separating when logic becomes harder to reuse or test.
Growing teams or production apps Enforce SRP early. Clean separation avoids tech debt.
Large codebases SRP is mandatory — otherwise, changes become dangerous.

Summary

Split when:



When does it start making sense to introduce Clean Architecture into a project?

That’s a deep and important question, and the answer depends on balancing project scope, risk, and maintenance horizon.

Minimum Scale Where Clean Architecture Becomes Worthwhile

My minimum: For a solo developer or small team, I’d adopt Clean Architecture principles (modular layers, separation of concerns, testability) as soon as:

That threshold can be surprisingly low — for example:

If your app:

…then going with a modular layered approach like Clean Architecture pays off early.


When Clean Architecture Is Overkill

Clean Architecture is not needed (and can actively slow you down) if:

Example: A flashlight app, a simple calculator, a QR scanner, you can write these with just an Activity and a couple helper classes. No need for UseCases, Repositories, DataSources, etc.


Heuristics to Decide

If you have… Consider
>1 data source (e.g., DB + API) ✅ Use layers (repository, datasource)
Business logic that must be tested ✅ Use usecases or interactors
Multiple devs / long-term codebase ✅ Clean separation helps scale teamwork
Tight deadline + short-lived MVP ❌ Keep it flat, maybe refactor later
Simple CRUD with no complex logic ❌ Stick to MVVM or even plain Activity + ViewModel
External SDKs or unstable APIs ✅ Use abstractions to decouple

Evolution Strategy (Start Simple → Grow into Clean)

You don’t need to go full Clean from day one.

Here’s a path:

  1. Start flat with clear separation: UI → ViewModel → Repo → Network/DB.
  2. As logic grows, extract UseCases to isolate operations.
  3. If the app keeps growing, split into domain, data, and presentation modules.
  4. Add interfaces and DI (like Hilt/Koin) to fully decouple and test.

This is sometimes called “clean-lite” or “onion-by-need”.


Summary

You should adopt Clean Architecture if:

You shouldn’t go full Clean if:


snippet.cpp
by:
▖     ▘▖▖
▌ ▌▌▛▘▌▙▌
▙▖▙▌▙▖▌ ▌
 
published: June 16 2025.