Understanding Hexagonal Architecture

Hexagonal Architecture, also known as the Ports and Adapters pattern, is a design principle that aims to create loosely coupled application components. This architecture enables systems to be highly testable, maintainable, and adaptable to changing requirements. While traditionally applied in backend systems, its principles can significantly benefit frontend development as well.

In this article, we will delve into the concept of Hexagonal Architecture, understand its core principles, and explore how to apply it in frontend development with practical coding examples.

Hexagonal Architecture was introduced by Alistair Cockburn in 2005. The main idea is to create an application that is independent of external elements, such as databases, UI, or third-party services. This independence is achieved through the use of ports and adapters.

Core Principles of Hexagonal Architecture

  1. Separation of Concerns: The application core is isolated from the outside world, promoting clean separation of business logic and external dependencies.
  2. Testability: By isolating the core, it becomes easier to write unit tests without requiring real implementations of external systems.
  3. Maintainability: Loose coupling ensures that changes in external systems do not heavily impact the core logic.
  4. Flexibility: The system can easily adapt to new requirements or technologies by swapping adapters without affecting the core logic.

Structure of Hexagonal Architecture

  1. Application Core: Contains the business logic and domain entities.
  2. Ports: Interfaces that define how the core interacts with the outside world.
  3. Adapters: Implementations of the ports, facilitating communication between the core and external systems.

Applying Hexagonal Architecture in Frontend Development

In the context of frontend development, Hexagonal Architecture can help manage complex UI logic, enhance testability, and improve maintainability. Let’s explore how to apply these principles in a React application.

Setting Up a React Project

To demonstrate Hexagonal Architecture, we’ll set up a simple React application using Create React App:

bash

npx create-react-app hexagonal-frontend
cd hexagonal-frontend

Defining the Application Core

The core of our frontend application will consist of business logic and domain models. For this example, we’ll create a simple to-do application.

Creating the Domain Model

Create a new directory called core and define the domain model:

javascript

// src/core/todo.js

export class Todo {
constructor(id, title, completed = false) {
this.id = id;
this.title = title;
this.completed = completed;
}

toggle() {
this.completed = !this.completed;
}
}

Creating Application Services

Next, we’ll define application services that interact with the domain model:

javascript

// src/core/todoService.js

import { Todo } from ‘./todo’;

export class TodoService {
constructor() {
this.todos = [];
}

addTodo(title) {
const newTodo = new Todo(
this.todos.length + 1,
title
);
this.todos.push(newTodo);
return newTodo;
}

toggleTodo(id) {
const todo = this.todos.find(todo => todo.id === id);
if (todo) {
todo.toggle();
}
}

getTodos() {
return this.todos;
}
}

Defining Ports

Ports in Hexagonal Architecture are interfaces that define interactions between the core and external systems. In a frontend application, these ports can be used to abstract data fetching and state management.

Creating Port Interfaces

We will define a port interface for fetching todos:

javascript

// src/ports/todoRepository.js

export class TodoRepository {
getTodos() {
throw new Error(‘Method not implemented.’);
}
}

Implementing Adapters

Adapters are concrete implementations of ports. In our example, we will create a simple in-memory adapter.

In-Memory Adapter

javascript

// src/adapters/inMemoryTodoRepository.js

import { TodoRepository } from ‘../ports/todoRepository’;

export class InMemoryTodoRepository extends TodoRepository {
constructor() {
super();
this.todos = [];
}

getTodos() {
return this.todos;
}

addTodo(todo) {
this.todos.push(todo);
}
}

Integrating the Core, Ports, and Adapters

Now that we have defined the core, ports, and adapters, we need to integrate them into our React components.

Setting Up the Application Context

We will use React’s context to provide the TodoService and InMemoryTodoRepository to our components.

javascript

// src/context/todoContext.js

import React, { createContext, useContext, useState } from ‘react’;
import { TodoService } from ‘../core/todoService’;
import { InMemoryTodoRepository } from ‘../adapters/inMemoryTodoRepository’;

const TodoContext = createContext();

export const TodoProvider = ({ children }) => {
const todoRepository = new InMemoryTodoRepository();
const todoService = new TodoService(todoRepository);

return (
<TodoContext.Provider value={{ todoService, todoRepository }}>
{children}
</TodoContext.Provider>

);
};

export const useTodoContext = () => {
return useContext(TodoContext);
};

Creating React Components

Finally, let’s create React components to interact with our application core.

ToDoList Component

javascript

// src/components/ToDoList.js

import React, { useState } from ‘react’;
import { useTodoContext } from ‘../context/todoContext’;

const ToDoList = () => {
const { todoService } = useTodoContext();
const [todos, setTodos] = useState(todoService.getTodos());

const handleAddTodo = (title) => {
todoService.addTodo(title);
setTodos(todoService.getTodos());
};

const handleToggleTodo = (id) => {
todoService.toggleTodo(id);
setTodos(todoService.getTodos());
};

return (
<div>
<h1>Todo List</h1>
<input
type=“text”
onKeyDown={(e) =>
{
if (e.key === ‘Enter’) {
handleAddTodo(e.target.value);
e.target.value = ”;
}
}}
/>
<ul>
{todos.map((todo) => (
<li key={todo.id} onClick={() => handleToggleTodo(todo.id)}>
<span style={{ textDecoration: todo.completed ? ‘line-through:none‘ }}>
{todo.title}
</span>
</li>
))}
</ul>
</div>

);
};

export default ToDoList;

App Component

javascript

// src/App.js

import React from ‘react’;
import { TodoProvider } from ‘./context/todoContext’;
import ToDoList from ‘./components/ToDoList’;

const App = () => {
return (
<TodoProvider>
<ToDoList />
</TodoProvider>

);
};

export default App;

Running the Application

With all components in place, you can start the application:

bash

npm start

Open http://localhost:3000 to view the application in your browser.

Benefits of Hexagonal Architecture in Frontend

Applying Hexagonal Architecture in frontend development offers several advantages:

  1. Enhanced Testability: With a clear separation of concerns, the business logic can be tested independently from the UI components.
  2. Improved Maintainability: Changes in external systems (e.g., APIs or UI frameworks) require minimal changes to the core logic.
  3. Increased Flexibility: The application can easily adapt to new technologies or requirements by adding or replacing adapters without altering the core logic.
  4. Better Organization: The architecture promotes a clean and well-organized codebase, making it easier to understand and extend.

Conclusion

Hexagonal Architecture provides a robust framework for creating flexible, maintainable, and testable frontend applications. By separating the core logic from external dependencies through ports and adapters, developers can build systems that are easier to manage and evolve over time. Implementing this architecture in a React application, as demonstrated, showcases its practical benefits and helps maintain a clean, scalable codebase.

By adopting Hexagonal Architecture, frontend developers can achieve greater control over their code, leading to more resilient and adaptable applications. Whether working on a simple project or a complex enterprise application, the principles of Hexagonal Architecture offer valuable guidelines for structuring and organizing code effectively.