DEV Community

Cover image for Clean Architecture: The Unattainable Ideal – A Parable for Developers
simprl
simprl

Posted on

Clean Architecture: The Unattainable Ideal – A Parable for Developers

High in the serene mountains of Tibet, nestled within the quiet halls of an ancient monastery, there lived a young Apprentice. He was deeply devoted to the pursuit of harmony—not only within himself but also in his craft of programming. His goal was to create a perfect application, one that would embody the profound principles of Clean Architecture, like the clarity of a mountain stream. But sensing the immense difficulty of this path, he sought the wisdom of a venerable Master.

The Apprentice approached the Master with humility and asked:

— “Oh, wise Master, I have built an application to manage purchases. Is my architecture clean and pure?”

The Master, observing the student with patient eyes, replied:

— “Show me what you have created, and together we shall discern the truth.”

The Apprentice revealed his work, where the database logic and the flow of use cases were intertwined—business logic woven tightly with the technical framework, like threads in a tangled net.

// app.ts
import sqlite3 from 'sqlite3';
import { open, Database } from 'sqlite';

interface Purchase {
    id: number;
    title: string;
    cost: number;
}

async function initializeDatabase(): Promise<Database> {
    const db = await open({
        filename: ':memory:',
        driver: sqlite3.Database,
    });

    await db.exec(`
    CREATE TABLE purchases (
      id INTEGER PRIMARY KEY,
      title TEXT,
      cost REAL
    )
  `);

    return db;
}

async function addPurchaseIfCan(db: Database, purchase: Purchase): Promise<void> {
    const { id, title, cost } = purchase;
    const row = await db.get<{ totalCost: number }>(
        `SELECT SUM(cost) as totalCost FROM purchases WHERE title = ?`,
        [title]
    );
    const totalCost = row?.totalCost || 0;
    const newTotalCost = totalCost + cost;

    if (newTotalCost < 99999) {
        await db.run(
            `INSERT INTO purchases (id, title, cost) VALUES (?, ?, ?)`,
            [id, title, cost]
        );
        console.log('Purchase added successfully.');
    } else {
        console.log('Total cost exceeds 99999.');
    }
}

(async () => {
    const db = await initializeDatabase();
    await addPurchaseIfCan(db, { id: 3, title: 'rice', cost: 2 });
})();

Enter fullscreen mode Exit fullscreen mode

After contemplating the code, the Master said:

— “Your code is like a river where the clear water of purpose is mixed with the mud of implementation. Business and technical concerns have become one, when they should flow separately. To achieve true purity in your architecture, you must divide them, as the sky is divided from the earth.”

The First Steps on the Path

Heeding the Master’s words, the Apprentice set about the task of restructuring his code. He began to separate the layers, drawing distinct boundaries between the database and the flow of business logic. He also introduced interfaces, aligning his code with the principle of Dependency Inversion, one of the sacred teachings of Clean Architecture. Now, his application no longer depended on specific implementations but on the abstraction of ideas.

// app.ts

import { initializeDatabase } from './db/init';
import { PurchaseRepository } from './db/purchaseRepository';
import { addPurchaseIfCan } from './useCases/addPurchaseIfCan';

(async () => {
  const db = await initializeDatabase();
  const purchaseRepository = new PurchaseRepository(db);

  await addPurchaseIfCan(purchaseRepository, { id: 3, title: 'rice', cost: 2 });
})();
Enter fullscreen mode Exit fullscreen mode
// useCases/addPurchaseIfCan.ts

import { IPurchaseRepository, Purchase } from './IPurchaseRepository';

export async function addPurchaseIfCan(
  purchaseRepository: IPurchaseRepository,
  purchase: Purchase
): Promise<void> {
  const { id, title, cost } = purchase;

  const totalCost = await purchaseRepository.getTotalCostByTitle(title);
  const newTotalCost = totalCost + cost;

  if (newTotalCost < 99999) {
    await purchaseRepository.add(purchase);
    console.log('Purchase added successfully.');
  } else {
    console.log('Total cost exceeds 99999.');
  }
}
Enter fullscreen mode Exit fullscreen mode
// useCases/IPurchaseRepository.ts

export interface IPurchaseRepository {
  add(purchase: Purchase): Promise<Purchase>;
  getTotalCostByTitle(title: string): Promise<number>;
}

export interface Purchase {
  id: number;
  title: string;
  cost: number;
}
Enter fullscreen mode Exit fullscreen mode
// db/init.ts

import sqlite3 from 'sqlite3';
import { open, Database } from 'sqlite';

export async function initializeDatabase(): Promise<Database> {
  const db = await open({
    filename: ':memory:',
    driver: sqlite3.Database,
  });

  await db.exec(`
    CREATE TABLE purchases (
      id INTEGER PRIMARY KEY,
      title TEXT,
      cost REAL
    )
  `);

  return db;
}
Enter fullscreen mode Exit fullscreen mode
// db/purchaseRepository.ts

import { Database } from 'sqlite';
import { IPurchaseRepository, Purchase } from 'useCases/IPurchaseRepository';

export class PurchaseRepository implements IPurchaseRepository {
  private db: Database;

  constructor(db: Database) {
    this.db = db;
  }

  async add(purchase: Purchase): Promise<Purchase> {
    const { id, title, cost } = purchase;
    await this.db.run(
      `INSERT INTO purchases (id, title, cost) VALUES (?, ?, ?)`,
      [id, title, cost]
    );
    return purchase;
  }

  async getTotalCostByTitle(title: string): Promise<number> {
    const row = await this.db.get<{ totalCost: number }>(
      `SELECT SUM(cost) as totalCost FROM purchases WHERE title = ?`,
      [title]
    );
    const totalCost = row?.totalCost || 0;
    return totalCost;
  }
}
Enter fullscreen mode Exit fullscreen mode

The Apprentice returned to the Master and asked:

— “I have separated the layers and used interfaces for my repository. Is my architecture now clean?”

The Master, reviewing the code once more, responded:

— “You have taken a step forward, but the calculation of total cost still dwells in the infrastructure, where it does not belong. This is not where such wisdom should reside. The knowledge of total cost belongs to the realm of business, not the tools of the earth. Move it into the use case, where the wisdom of the process may be preserved in its purity.”

The Lesson of Separation

With this insight, the Apprentice realized that the calculation of total cost was part of the business logic. He refactored his code again, moving the logic into the use case, keeping the business concerns free from the grasp of technical infrastructure.

// useCases/IPurchaseRepository.ts

export interface IPurchaseRepository {
  add(purchase: Purchase): Promise<Purchase>;
-  getTotalCostByTitle(title: string): Promise<number>;
+  getPurchasesByTitle(title: string): Promise<Purchase[]>;
}
...
Enter fullscreen mode Exit fullscreen mode
// useCases/addPurchaseIfCan.ts

import { IPurchaseRepository, Purchase } from './IPurchaseRepository';

export async function addPurchaseIfCan(
  purchaseRepository: IPurchaseRepository,
  purchaseData: Purchase,
  limit: number
): Promise<void> {
  const { id, title, cost } = purchaseData;

  const purchases = await purchaseRepository.getPurchasesByTitle(title);

  let totalCost = 0;
  for (const purchase of purchases) {
    totalCost += purchase.cost;
  }

  const newTotalCost = totalCost + cost;

  if (newTotalCost >= limit) {
    console.log(`Total cost exceeds ${limit}.`);
  } else {
    await purchaseRepository.add(purchaseData);
    console.log('Purchase added successfully.');
  }
}
Enter fullscreen mode Exit fullscreen mode
// db/purchaseRepository.ts

import { Database } from 'sqlite';
import { IPurchaseRepository } from './IPurchaseRepository';

export class PurchaseRepository implements IPurchaseRepository {

  ...

  async getPurchasesByTitle(title: string): Promise<Purchase[]> {
    const rows = await this.db.all<Purchase[]>(
      `SELECT * FROM purchases WHERE title = ?`,
      [title]
    );
    return rows.map((row) => ({
      id: row.id,
      title: row.title,
      cost: row.cost,
    }));
  }
}
Enter fullscreen mode Exit fullscreen mode

Returning once more to the Master, he asked:

— “I have moved the total cost calculation into the use case and separated the business logic from the infrastructure. Is my architecture now pure?”

With a gentle smile, the Master replied:

— “You have made great progress, but beware—just as the mountain winds carry the chill of winter, your calculations may carry hidden errors. The arithmetic of JavaScript, like the mind of a novice, can be imprecise when dealing with large or small numbers.”

The Encounter with Impermanence

The Apprentice understood that the flaws of floating-point arithmetic in JavaScript could lead to subtle but dangerous mistakes. He revised his code, turning to a more reliable tool, a library designed for accurate calculations, seeking clarity in his work.

// useCases/addPurchaseIfCan.ts
+ import Decimal from 'decimal.js';
import { IPurchaseRepository, Purchase } from './IPurchaseRepository';

export async function addPurchaseIfCan(
  purchaseRepository: IPurchaseRepository,
  purchaseData: Purchase,
  limit: number
): Promise<void> {
  const { id, title, cost } = purchaseData;

  const purchases = await purchaseRepository.getPurchasesByTitle(title);

  let totalCost = new Decimal(0);
  for (const purchase of purchases) {
-    totalCost += purchase.cost;
+    totalCost = totalCost.plus(purchase.cost);
  }

- const newTotalCost = totalCost + cost;
+ const newTotalCost = totalCost.plus(cost);

- if (newTotalCost >= limit) {
+ if (newTotalCost.greaterThanOrEqualTo(limit)) {
    console.log(`Total cost exceeds ${limit}.`);
  } else {
    await purchaseRepository.add(purchaseData);
    console.log('Purchase added successfully.');
  }
}
Enter fullscreen mode Exit fullscreen mode

Once again, he asked the Master:

— “I have refined my calculations, using a better tool to avoid the errors. Has my architecture now reached its purity?”

The Master, his gaze steady as the mountains, replied:

— “You have done well, yet still, your architecture remains bound. Your business logic now depends on the details of this new tool decimal.js. Should you one day need to change this tool, the foundation of your logic will tremble. True purity is freedom from such bonds.”

The Wisdom of Dependency Inversion

Realizing the depth of the Master’s words, the Apprentice sought to free his code from such attachments. He abstracted the arithmetic operations, inverting the dependencies so that his business logic would no longer be tied to any one tool.

// useCases/calculator.ts
export abstract class Calculator {
  abstract create(a: number): Calculator;
  abstract add(b: Calculator | number): Calculator;
  abstract greaterThanOrEqual(b: Calculator | number): boolean;
}
Enter fullscreen mode Exit fullscreen mode
// useCases/addPurchaseIfCan.ts
+ import { Calculator } from 'useCases/calculator';
- import Decimal from 'decimal.js';
import { IPurchaseRepository, Purchase } from './IPurchaseRepository';
Enter fullscreen mode Exit fullscreen mode
// decimalCalculator.ts

import Decimal from 'decimal.js';
import { Calculator } from 'useCases/calculator.ts';

export class DecimalCalculator extends Calculator {
  private value: Decimal;

  constructor(value: number | Decimal) {
    super();
    this.value = new Decimal(value);
  }

  create(a: number): Calculator {
    return new DecimalCalculator(a);
  }

  add(b: Calculator | number): Calculator {
    return new DecimalCalculator(this.value.plus(b.value));
  }

  greaterThanOrEqual(b: Calculator | number): boolean {
    return this.value.greaterThanOrEqualTo(b.value);
  }
}
Enter fullscreen mode Exit fullscreen mode
// useCases/addPurchaseIfCan.ts

import { Calculator } from 'useCases/calculator';
import { IPurchaseRepository, Purchase } from './IPurchaseRepository';

export class addPurchaseIfCan {
  private purchaseRepository: IPurchaseRepository;
  private calculator: Calculator;
  private limit: string;

  constructor(
    purchaseRepository: IPurchaseRepository,
    calculator: Calculator,
    limit: number
  ) {
    this.purchaseRepository = purchaseRepository;
    this.calculator = calculator;
    this.limit = limit.toString();
  }

  async execute(purchaseData: Purchase): Promise<void> {
    const { id, title, cost } = purchaseData;

    const purchases = await this.purchaseRepository.getPurchasesByTitle(title);

    let totalCost = this.calculator.create(0);
    for (const purchase of purchases) {
      totalCost.add(purchase.cost);
    }

    totalCost = totalCost.add(cost);

    if (totalCost.greaterThanOrEqual(this.limit)) {
      console.log(`Total cost exceeds ${limit}.`);
    } else {
      await this.purchaseRepository.add({
        id,
        title,
        cost: parseFloat(cost.toString()),
      });
      console.log('Purchase added successfully.');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Returning to the Master for what he hoped would be the final time, he asked:

— “I have abstracted my operations using dependency inversion, ensuring that my business logic is no longer tied to the implementation. Is my architecture now truly clean?”

The Master spoke:

— “You have traveled far on this path. But remember, even as you strive for purity, your use cases still depend on the language in which they are written. You walk with JavaScript and TypeScript now, but one day these tools may be no more. When that day comes, will you rebuild everything in a new language?”

The Embrace of Imperfection

The Apprentice, puzzled by this realization, asked:

— “Master, how can I achieve perfect cleanliness in my architecture if my use cases are always tied to the language in which they are written?”

The Master, with a soft smile of understanding, answered:

— “Just as a bird cannot leave the sky, architecture cannot be fully separate from the tools of its creation. True independence is a noble dream, but it is unattainable. The pursuit of it, however, brings harmony to your architecture. The goal of Clean Architecture is not to escape all dependencies, but to create a system where change is met with ease, and where the wisdom of business is separated from the workings of the earth. Understanding this balance is the key to true wisdom.”

The Apprentice, feeling the light of understanding dawn within him, said:

— “Thank you, Master. Now I see that perfection is not in isolation, but in the harmony of responsibility and purpose.”

The Master, rising from his seat, replied:

— “Go in peace, Apprentice. Your journey is just beginning, but you have already found your way.”

Epilogue

As time passed, the Apprentice noticed that his application had begun to slow. Confused, he wondered how a system that once ran so smoothly now struggled with its tasks.

It became clear that the issue was not the growing size of the code, but the fact that the total cost calculation was happening outside of the database. The application was spending its strength transferring vast amounts of data, only to perform a task that could have been done at the source. If the calculation had been done within the database, there would have been no need to send thousands of records between layers, and the performance would have remained strong.

The Apprentice wanted to ask the Master about this, but the Master had vanished, and the question remained unanswered.

Looking out over the quiet monastery, the Apprentice picked up a new book and, with a smile, said:

— “It seems my path to enlightenment has led me to a new challenge — the art of performance optimization.”

Top comments (0)