Hexagonal Architecture

Basic introduction using Python

2022-10 Victor Dorneanu

1. Introduction

Why should you chose hexagonal architecture ?

  • clean architecture helps with Secure by Design™
    • adopt “shift left” mentality and guarantee (application) Security
    • add Security to the early stages of the SDLC
  • clear separation of concerns (fundamental in Information hiding)
  • help with technical debt

1.1. Hexagonal Architecture in a nutshell

1.1.1. It’s all about business logic

  • explicit separation between what code is internal to the application and what is external

The idea of Hexagonal Architecture is to put inputs and outputs at the edges of our design. Business logic should not depend on whether we expose a REST or a GraphQL API, and it should not depend on where we get data from — a database, a microservice API exposed via gRPC or REST, or just a simple CSV file.
Ready for changes with hexagonal architecture | netflix blog

1.2. Ports & Adapters

1.2.1. Ports

  • A port is an input to your application and the only way the external world can reach it
  • Examples:
    • HTTP/gRPC servers handling requests from outside to your business application
    • CLI commands doing something with your business use cases
    • Pub/Sub message subscribers

1.2.2. Adapters

  • Adapters are something that talk to the outside world
  • you have to adapt your internal data structures to what it’s expected outside
  • Examples
    • SQL queries
    • HTTP/gRPC clients
      • file readers/writers
  • Some distinguish between
    • primary/driving adapters
    • secondary/driven adapters

1.2.3. Application logic/core

  • a layer that glues together other layers
  • also known as the place where “use cases” live at
  • this is what our code is supposed to do (it’s the main application)
  • the application logic depends only on own domain entities
    • if you cannot say which database is used for storing entities, that’s a good sign
    • if you cannot say which URLs it calls for doing authentication, that’s a good sign
    • in general: this layer is “free” of any concrete implementation details

1.3. Language-agnostic implementation

  • I’ll describe use cases for a concrete problem
  • There are several actors involved
    • uploader
    • product manager
  • I’ll abstractions to define relationships between
    • use cases and
    • concrete implementations

1.3.1. Uploader: use case description

As an uploader I’d like to upload documents to some infrastructure. After successful upload I’d like to get a link I can share with my friends.
– Uploader

Easy! Some observations:

  • the uploader doesn’t mention where (storage) the documents should be uploaded to
  • the uploader doesn’t mention how he/she would like to upload documents
    • via web client?
    • via mobile phone?
    • via CLI?

1.3.2. Product Manager: use case description

As a product manager I’d like to see how many documents each uploader uploads and how many times he/she shares the link.
– Product Manager

Also easy! Again some observations:

  • PM doesn’t mention where the metrics should be sent to
  • PM doesn’t mention how she would like to view the metrics
    • Via Web?
    • On her smartphone?
    • Using the CLI?

1.3.3. Use abstractions

  • post-pone decision about concrete implementation details / concrete technologies
  • focus on business cases and use abstractions (aka interfaces) whenever possible
  • separate concerns
  • you can apply this on different levels

2. Software Architecture: High-Level

Figure 3: Architecture of some imaginary application which uploads some documents to a storage system

2.1. Software Architecture: High-Level (explanations)

  • The Business Domain ❶ contains
    • Entities (there is only Document)
    • Services (DocumentUploadService)
    • Repositories (DocumentStorageRepository and DocumentMetricsRepository)
      • basically interfaces to be implemented by the Secondary Adapters
  • The Secondary (Driven) Adapters implement
    • the repositories/interfaces defined in the Business Domain
  • The Primary (Driving) Adapters ❷ use the Services
    • a CLI could implement the DocumentUploadService for the terminal
    • a HTTP server could serve the DocumentUploadService via HTTP

3. Domain

  • everything related to the business case
    • uploader wants to upload some document
    • PM wants to have some metrics
  • contains
    • Entities
    • Services
    • Repositories/Interfaces

Figure 4: The business domain contains the application login and uses abstractions (interfaces) for defining interactions.

3.1. Entities

class Document ():                        ❶
	"""A document is an entity"""
	def __init__(self, path: FilePath):
		self._file_path = path            ❷

	def meta(self):
	"""Display meta information about the file"""
		print("Some information")
  • We only have Document ❶ as an entity
  • The constructor will set an instance variable ❷ for storing the file path

3.2. Services

3.2.1. UploadDocumentService

class UploadDocumentService:           ❶
	"""Upload a document to storage repository"""

	def __init__(
		storage_repository: DocumentStorageRepository,
		metrics_repository: DocumentMetricsRepository,
		self._storage_repo: DocumentStorageRepository
		self._metrics_repo: DocumentMetricsRepository

	def upload_document(self, document: Document):   ❷
  • ❶ We have an UploadDocumentService
  • ❷ this service implements upload_document(document: Document)

4. Repositories

The repositories are basically interfaces for the secondary (driven) adapters. In our case we have:

  • a repository for dealing with document storage
    • define how to save documents
    • define how to search for documents
    • define how to delete a document
  • a repository for dealing with metrics