Adopting Hexagonal Architecture in Serverless

Ivano García
5 min readJul 11, 2023

Hexagonal Architecture, also known as Ports and Adapters, is an architectural pattern aimed at creating a decoupled and easily testable system. This pattern is especially useful in complex projects where the business logic needs to be isolated from the infrastructure layer and technical details.

In Serverless, we adopt a serverless approach to building our applications. This is a way to build and run applications and services without having to manage infrastructures. Serverless allows you to run your code without provisioning or managing servers, allowing you to innovate more quickly and reduce the total cost of ownership. It also provides automatic scaling, from a few requests per day to thousands per second.

But, how can we combine these two powerful concepts? That’s what we will explore in this article, using an example of a service to retrieve tasks.

Hexagonal Architecture Proposal for Serverless

The fundamental principle of hexagonal architecture is that dependencies always point inward, which means that your business logic does not depend on any external technology, but external technologies depend on your business logic. In Hexagonal Architecture, we divide our application into three main layers: the Domain layer, the Application layer, and the Infrastructure layer.

Below is a proposed project structure for a Serverless application with a hexagonal architecture:

Directory Descriptions

  • domain: This layer contains business logic and domain entities. It should not depend on any other layer.
  • domain/contracts: Here are the interfaces that define the methods that our repository should implement.
  • domain/entities: Contains the objects and their representation in the system.
  • application: This layer acts as a bridge between the Domain layer and the Infrastructure layer. It contains use cases, which are high-level operations that the application can perform.
  • application/services: A directory that will store the use cases of our system.
  • infrastructure: This layer contains all the technical details and implementation, such as the database, user interface, frameworks, etc.
  • infrastructure/dependencies: Includes the configuration for the dependency container.
  • infrastructure/lambda: This is one of our main directories and the core of our serverless architecture. Here are our lambda functions and the handlers in charge of them.
  • infrastructure/lambda/handler: As our main focus is Serverless, we replace our controller directory with handler. We maintain the concept and definition unique to Serverless and, additionally, include an additional directory that will handle versions (v1).
  • infrastructure/repositories: Here we refer to the concrete implementations of data repositories. In simpler terms, it is the interface between the application code and the database or other data source.
  • libs: Here we will include those methods and libraries that support the functioning of our project, such as error handlers, API Gateway control, and middleware.

These directories represent a basic skeleton of a Serverless application with a hexagonal architecture. By decoupling business logic from implementation details, you can make your application more maintainable, scalable, and testable, which is especially valuable in a serverless development environment.

Understanding with code

Let’s create the previous example using Hexagonal Architecture. For this, we will use Awilix for dependency injection.

You can install the package executing:

yarn add aws-lambda serverless awilix reflect-metadata typescript ts-node @types/node @types/aws-lambda

Note: We must install our package in our serverless project. Our focus in the post is to explain the Hexagonal architecture with the serverless framework and not how it works Serverless Framework, we can look at it in the coming soon posts

Domain Layer (Entities and Contracts)

// src/domain/entities/TaskEntity.ts

export class Task {
constructor(
public id: string,
public title: string,
public description: string) {}
}
// src/domain/contracts/TaskRepositoryInterface.ts

import { Task } from '../entities/TaskEntity';
export interface ITaskRepository {
getTasks(): Promise<Task[]>;
}

The Task entity is a simple object representing a task in our system. ITaskRepository is an interface that defines the methods that our task repository must implement.

Application Layer (Services or Use Cases)

// src/application/services/TaskService.ts

import { Task } from '../domain/entities/TaskEntity';
import { ITaskRepository } from '../domain/contracts/TaskRepositoryInterface';
export class TaskService {
constructor(private readonly taskRepository: ITaskRepository) {}
public async getTasks(): Promise<Task[]> {
return this.taskRepository.getTasks();
}
}

TaskService is a use case that retrieves the tasks from our system.

Infrastructure Layer (Handler and Repositories)

// src/infrastructure/repositories/TaskRepository.ts

import { Task } from '../../domain/entities/TaskEntity';
import { ITaskRepository } from '../../domain/contracts/TaskRepositoryInterface';
export class TaskRepository implements ITaskRepository {
public async getTasks(): Promise<Task[]> {
// Here would be the code to get the tasks from a database
return [];
}
}
// src/infrastructure/lambda/task/handler/v1/handler.ts

import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { TaskService } from '../../application/services/TaskService';
import { container } from '../di-container';
export async function getTasksHandler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
const taskService = container.resolve<TaskService>('taskService');
const tasks = await taskService.getTasks();
return {
statusCode: 200,
body: JSON.stringify(tasks),
};
}

Finally, we register our dependencies in Container.ts:

// src/infrastructure/dependencies/Container.ts

import { asClass, createContainer, InjectionMode } from 'awilix';
import { TaskService } from '../application/services/TaskService';
import { TaskRepository } from './repositories/TaskRepository';
export const container = createContainer({
injectionMode: InjectionMode.CLASSIC,
}).register({
taskService: asClass(TaskService).scoped(),
taskRepository: asClass(TaskRepository).scoped(),
});

In this explanation, we maintain the concept of separation of responsibilities and follow the SOLID principles and Hexagonal Architecture. Each layer has its own responsibility and is decoupled from the others. Dependencies are injected in a suitable way, making the system more maintainable, scalable, and testable.

Conclusion

Serverless Framework, can transform how we structure and design our applications. Using dependency injection and applying the SOLID principles, we’ve created an application that’s modular, easily testable, and allows for the quick swapping of different infrastructure components.

In our application, the Domain Layer contains business logic and entities, completely agnostic to any technology. The Application Layer bridges the Domain Layer and Infrastructure Layer, facilitating high-level operations. The Infrastructure Layer, on the other hand, is where all technical and implementation details, like databases, UI, frameworks, etc., are taken care of.

By applying the Hexagonal Architecture and Awilix for dependency injection, we’ve managed to build a simple yet robust system. The task service communicates with a task repository, abstracted through an interface. Changing the task repository’s data source, be it from endpointA to endpointB or from a relational database to a NoSQL one, only involves changing the repository’s implementation, leaving the task service undisturbed. This decoupling makes the application easier to manage, scale, and test.

In conclusion, Hexagonal Architecture is not only a game-changer for complex projects but also an exceptional design pattern for Serverless applications. It ensures the technology agnostic nature of your business logic, allows the ability to swap infrastructure components with ease, and encourages a clean, maintainable, and scalable codebase. Remember, in Hexagonal Architecture, the key is separation of concerns and the inward flow of dependencies. When properly utilized, it can elevate your Serverless applications to new heights of efficiency and maintainability.

Thus, the Hexagonal Architecture offers an effective way to leverage the benefits of the Serverless Framework while avoiding common architectural pitfalls, creating applications that are resilient, flexible, and primed for future growth.

--

--

Ivano García

Systems engineer at @adidas, Blockchain developer, technology and innovation lover