Hexagonal Architecture in Python

Hexagonal architecture is an architectural pattern in software engineering where certain boundaries/interfaces are defined as edges of a hexagon. The pattern is also known as the ports and adapters pattern, which is more descriptive to the actual implementation.

Photo by James Hon on Unsplash
domain/
├── vote.py
└── vote_repository.py
import uuid
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:
# This is necessary to prevent circular imports
from app.domain.vote_repository import VoteRepository


@dataclass
class Vote:
vote_id: str = field(default_factory=lambda: str(uuid.uuid4()))

def save(self, vote_repository: 'VoteRepository'):
return vote_repository.add(self)

def __hash__(self):
return hash(self.vote_id)
import abc
from typing import List

from app.domain.vote import Vote


class VoteRepository(metaclass=abc.ABCMeta):
@abc.abstractmethod
def add(self, vote: Vote) -> Vote:
raise NotImplementedError

@abc.abstractmethod
def all(self) -> List[Vote]:
raise NotImplementedError

@abc.abstractmethod
def total(self) -> int:
raise NotImplementedError
import uuidfrom app.domain.vote import Vote
def test_vote_existing_vote_id():
vote_id = str(uuid.uuid4())
assert Vote(vote_id).vote_id == vote_id
def test_vote_defaults():
vote_id = str(uuid.uuid4())
assert Vote().vote_id != vote_id
voting-system/
├── app/
│ ├── adapter/
│ │ └── inmemory_vote_repository.py
│ └── domain/
│ ├── vote.py
│ └── vote_repository.py
├── main.py
└── tests/
├── adapter/
│ └── test_inmemory_vote_repository.py
└── domain/
└── test_vote.py
from typing import List

from app.domain.vote import Vote
from app.domain.vote_repository import VoteRepository


class InMemoryVoteRepository(VoteRepository):
def __init__(self):
self.votes = []

def add(self, vote: Vote) -> Vote:
self.votes.append(vote)
return vote

def all(self) -> List[Vote]:
return self.votes

def total(self) -> int:
return len(self.votes)
from app.adapter.inmemory_vote_repository import InMemoryVoteRepository
from app.domain.vote import Vote


def test_vote_save():
vote = Vote()
vote_repository = InMemoryVoteRepository()

assert vote.save(vote_repository).vote_id == vote.vote_id


def test_vote_repository_all():
vote_repository = InMemoryVoteRepository()
vote1 = Vote().save(vote_repository)
vote2 = Vote().save(vote_repository)

assert set(vote_repository.all()) == {vote1, vote2}


def test_vote_repository_total():
vote_repository = InMemoryVoteRepository()
Vote().save(vote_repository)
Vote().save(vote_repository)

assert vote_repository.total() == 2
from app.adapter.inmemory_vote_repository import InMemoryVoteRepository
from app.domain.vote import Vote


def main():
vote_repository = InMemoryVoteRepository()

Vote().save(vote_repository)
Vote().save(vote_repository)

print(vote_repository.all())
print(f'Total votes: {vote_repository.total()}')


if __name__ == '__main__':
main()
  1. The adapter
  2. The main entry point
from app.adapter.inmemory_vote_repository import InMemoryVoteRepository
from app.domain.vote import Vote

from fastapi import FastAPI

app = FastAPI()

vote_repository = InMemoryVoteRepository()


@app.post("/vote", response_model=Vote)
def vote() -> Vote:
return Vote().save(vote_repository)


@app.get("/votes", response_model=int)
def votes() -> int:
return vote_repository.total()
voting-system/
├── app/
│ ├── adapter/
│ │ └── inmemory_vote_repository.py
│ ├── domain/
│ │ ├── vote.py
│ │ └── vote_repository.py
│ └── main.py
├── main.py
└── tests/
├── adapter/
│ └── test_inmemory_vote_repository.py
├── api/
│ ├── test_get_votes.py
│ └── test_post_vote.py
├── conftest.py
├── domain/
│ └── test_vote.py
└── fixtures/
└── client.py
from tests.fixtures import *  # NOQA
import pytest
from starlette.testclient import TestClient


@pytest.fixture
def client():
from app.main import app

return TestClient(app)
def test_post_vote(client):
response = client.post("/vote")
assert response.status_code == 200
def test_get_votes_0(client):
response = client.get("/votes")
assert response.status_code == 200
assert response.json() == 0


def test_get_votes_1(client):
client.post("/vote")
response = client.get("/votes")
assert response.status_code == 200
assert response.json() == 1


def test_get_votes_10(client):
for i in range(1, 10):
client.post("/vote")
response = client.get("/votes")
assert response.status_code == 200
assert response.json() == 10

Software Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store