Clean Architecture

2023-05-14

As projects grow bigger, the code tends to get messy if there is no clear structure. I have worked on projects where everything was mixed together, and making a small change in one place would break something completely unrelated. Clean architecture is an approach to organising code so that each part has a clear responsibility and changes in one area do not ripple through the entire codebase.

Separation of Concerns

The core idea is to separate the code into layers, where each layer has a specific job. A common way to organise this is:

Presentation layer handles the UI. In a React app, this would be the components, pages, and anything related to what the user sees and interacts with.

Business logic layer contains the rules and logic of the application. For example, how to calculate a discount, what conditions must be met before placing an order, or how to validate user input beyond simple format checking.

Data layer handles communication with external sources like APIs, databases, or local storage. This includes the fetch calls, data transformations, and error handling for external requests.

Why It Matters

When these layers are mixed together, we end up with components that fetch data, apply business rules, and render the UI all in one place. This makes the component hard to test because we cannot test the business logic without rendering the UI. It also makes it hard to reuse the logic because it is tied to a specific component.

By separating them, we can test the business logic independently, reuse it across different components, and swap out the data source without touching the rest of the code.

Practical Example

Instead of putting everything in a React component:

function OrderPage() {
  const [orders, setOrders] = useState([]);
  
  useEffect(() => {
    fetch("/api/orders")
      .then(res => res.json())
      .then(data => {
        const filtered = data.filter(o => o.status !== "cancelled");
        const sorted = filtered.sort((a, b) => b.total - a.total);
        setOrders(sorted);
      });
  }, []);
  
  return <OrderList orders={orders} />;
}

We can separate it:

// data layer
function fetchOrders() {
  return fetch("/api/orders").then(res => res.json());
}

// business logic
function getActiveOrdersByTotal(orders) {
  return orders
    .filter(o => o.status !== "cancelled")
    .sort((a, b) => b.total - a.total);
}

// presentation
function OrderPage() {
  const [orders, setOrders] = useState([]);
  
  useEffect(() => {
    fetchOrders().then(data => {
      setOrders(getActiveOrdersByTotal(data));
    });
  }, []);
  
  return <OrderList orders={orders} />;
}

The component is now simpler. The fetch logic and business logic can be tested on their own. If the API endpoint changes, we only update the data layer. If the filtering rules change, we only update the business logic.

Folder Structure

A simple folder structure that reflects this separation could look like:

src/
  components/
  pages/
  services/
  utils/
  hooks/

services for the data layer, utils for shared business logic, hooks for reusable stateful logic, and components and pages for the presentation layer. The exact structure depends on the project size, but the principle stays the same: keep each layer in its own place.