Hexagonal Architecture in Lambda Handlers with Middy
Preface
This post covers:
- The benefits of hexagonal architecture (ports & adapters) in AWS Lambda functions.
- How Middy enables clean separation of concerns.
- How to structure Lambda handlers using primary adapters.
Solution
Hexagonal architecture (also known as the Ports & Adapters pattern) helps separate business logic from external dependencies. This allows for:
- Easier testing β Business logic can be tested without needing actual AWS services.
- Greater flexibility β Adapters allow you to swap out services (e.g., replace DynamoDB with PostgreSQL).
- Improved maintainability β Handlers remain small and focused on coordination, not implementation.
Why Use Middy?
Middy is a middleware engine for AWS Lambda that simplifies handler composition by decoupling common concerns from business logic.
Key Benefits:
- Modular Middleware System β Common functionalities like logging, error handling, and authentication can be added as reusable middleware rather than being written inside each handler.
- Better Code Separation β It enforces separation of concerns, ensuring that Lambda handlers focus only on application logic.
- Improved Readability & Maintainability β By abstracting common concerns, it reduces boilerplate and makes code easier to follow.
- Enhanced Testability β With cleaner separation, individual parts of the Lambda function can be unit-tested independently.
Example Structure
// handler.ts
import middy from '@middy/core';
import { fetchPostHandler } from './adapters/primary/fetchPostHandler';
// imports for lambda powertools
export const handler = middy(fetchPostHandler)
.use(injectLambdaContext(logger)) // Adds Lambda context properties (e.g., AWS request ID) to the logger
.use(captureLambdaHandler(tracer)) // Captures tracing data for performance monitoring and debugging
.use(logMetrics(metrics)) // Collects and logs custom application metrics for monitoring
.use(httpErrorHandler()) // Automatically catches and formats thrown errors as HTTP responses
.use(makeHandlerIdempotent({ persistenceStore, config: idempotencyConfig })); // Ensures idempotency by preventing duplicate request processing
Adapter Layer
Your primary adapter (fetchPostHandler.ts
) only contains application logic:
// adapters/primary/fetchPostHandler.ts
import { fetchPostUseCase } from '../../core/useCases/fetchPost';
export const fetchPostHandler = async (event) => {
return fetchPostUseCase(event.pathParameters.id);
};
Use Case Layer
Business logic remains independent:
// core/useCases/fetchPost.ts
import { postRepository } from '../ports';
export const fetchPostUseCase = async (id) => {
return postRepository.getById(id);
};
Repository Layer (Secondary Adapter)
Allows switching between storage solutions:
// adapters/secondary/dynamoPostRepository.ts
import { DynamoDB } from 'aws-sdk';
const dynamoDb = new DynamoDB.DocumentClient();
export const postRepository = {
getById: async (id) => {
const result = await dynamoDb.get({ TableName: 'Posts', Key: { id } }).promise();
return result.Item;
}
};
Why?
- Keeps Lambda handlers lightweight β Only delegates to primary adapters.
- Encapsulates business logic β Makes testing and iteration easier.
- Allows flexibility β Swap out dependencies without rewriting core logic.
- Leverages Middy for reusability β Avoids duplicating logic like logging and error handling.
By structuring Lambda handlers with Middy and Hexagonal Architecture, you create more testable, maintainable, and scalable serverless applications.
My Technical Skills

AWS

JavaScript

TypeScript

React

Next.js

Cypress

Figma
