Member-only story
Become a better software engineer
Write Python Apps using Layered Architecture and Design Patterns
Layered architecture is probably the best-known architecture in software development industry and it’s a great pick when you want to develop a product with high testability score, little complexity and ease of development.
Opening Note
This is a complete and practical guide with full project code about Layered Architecture. Even if the code in this is written in Python, these ideas are general programming concepts and design patterns that can be implemented almost the same way in any OO Language like C++, Java, etc. This is a Zero-To-Hero guide where we will learn everything about the Layered Architecture including advanced stuff and other Design Patterns. Enjoy reading!
{ This guide was written without any assistance from any AI Tools }
In this guide I will show you how to use this architecture and other design patterns (like validator pattern, repository, decorator, etc) to create python applications that can be tested with ease, be scalable, and it will save you a lot (and I mean a lot) of development time in the long run. And the best part is that this is a general-purpose architecture that can be implemented in any language, and can be a backbone architecture for every project you start or work on. I will keep it short with the theory part and I will try to explain more with code.
All github code is available at the end of this guide.
In a typical Layered Architecture you would have the application components split into horizontal layers (the number of layers depends on the complexity of the app). The most common layers you will see with this architecture are:
- Presentation Layer
- Business Layer
- Persistence Layer
- Other Layers
1. Presentation Layer
This is known as the UI of your application. This can be a GUI or a Console UI which is responsible for displaying informations to the screen, displaying button, or whatever you want.
The most important thing to remember here is that you should never do ui logic in other components. For example, let’s say you build an app, you should never do UI stuff in other components, like prints, displaying things or interacting with UI. This kind of logic you have to do in the Presentation Layer. We will see how this works later in the guide, when we will implement an app using this architecture.
2. Business Layer
This layer is responsible with all your application’s business logic like sorting, filtering, generating reports, etc. Everything that it’s related to the business logic should be done in the Business Layer only. No generating reports, sorting, filtering, or stuff like that should be present in the presentation layer or on the model itself. If you are using other design patterns like Repository Pattern, it’s ok to perform sorting / filtering at database level since it could improve the performance by a lot, but be very careful, if you work with multiple sources of data (like database, files, etc) this can become a pain and you should avoid it in this scenario, if possible.
3. Persistence Layer
Here is where our application is managing the data, like storing into memory, database or a file, fetching the data, deleting data, and so on. You will understand more about how all layers work when we will implement our application.
4. Other Layers
Those layers can be anything else, like a Database Layer, which will be responsible for managing the database, opening connections, closing connections, making queries and so on. You can also add other Layers like Caching Layer, Domain Layer, Service Layer, etc.
In our application we would also use other layers so you can understand how those layers work.
Implementing an app with Layered Architecture and Design Patterns
So now that you have an ideea about how this architecture should look like, it’s time to put those ideas in practice and create a Python application following those rules. At the end I will talk abou other things related to this architecture so keep reading if you want to learn more.
Creating a business client management application
For this demonstration, let’s create an application that can manage the clients of a business and generate reports.
Let’s start by creating the client object. For this, we will be using another layer, the Domain Layer, for all business entities and for the Persistence Layer we will use the Repository Design Pattern.
In the begining our project structure should look like this:
├── console.py # Presentation Layer
├── domain # Domain Layer
│ ├── __init__.py
│ └── client.py # Business Entity
├── main.py # Application Entry Point
├── repository.py # Persistence Layer (Repository Design Pattern)
└── service.py # Business Layerdomain/client.py
This is a basic client entity, nothing to explain here.
class Client:
def __init__(
self,
client_id: str,
name: str,
email: str,
phone_number: str
):
self._client_id = client_id
self._name = name
self._email = email
self._phone_number = phone_number
self._address = None
@property
def client_id(self):
return self._client_id
@property
def name(self):
return self._name
@name.setter
def name(self, value: str):
self._name = value
@property
def email(self):
return self._email
@email.setter
def email(self, value: str):
self._email = value
@property
def phone_number(self):
return self._phone_number
@phone_number.setter
def phone_number(self, value: str):
self._phone_number = value
@property
def address(self):
return self._address
@address.setter
def address(self, value: str):
self._address = valuerepository.py
In the repository we will have the Persistence Layer, so this is responsible for managing the data. It can be a Memory Repository (we will store data in the memory), can be a File Repository (we will store data in files), can be a Database Repository (you got the point), etc.
The repository has 5 basic methods, that should allow us to do everything we want with data:
def get(self, client_id: str) -> Client:
# Return the client with the given client_id.
def get_all(self) -> List[Client]:
# Return a list of all the clients in the repository.
def save(self, client: Client) -> None:
# Add the client to the repository.
def update(self, client_id: str, client: Client) -> Client:
# Update the client in the repository and return the updated client.
def delete(self, client_id: str) -> Client:
# Remove the client from the repository and return the removed client.Now, if we want to write great code, we should first create an Interface for our repository (think about it as abstract class and polymorphism), and then implement the Memory Repository. From here if we want a File Repository to store the data in a file and actually persist that data, we can use the decorator pattern to do it. I will show it in a few moments.
Our project structure will become:
├── console.py
├── domain
│ ├── __init__.py
│ └── client.py
├── main.py
├── repository # This will become a Python module
│ ├── __init__.py
│ ├── interface.py # This will be the interface of our repository
│ └── memory.py # This will be our implementation for the memory repository
└── service.pyrepository/interface.py
This will be the interface of our repository.
from typing import Protocol
from typing import List
from domain.client import Client
class IRepository(Protocol):
""" Repository for Client CRUD operations """
def get(self) -> Client:
""" Get a specific client """
raise NotImplementedError("get not implemented")
def get_all(self) -> List[Client]:
""" Get all clients """
raise NotImplementedError("get_all not implemented")
def save(self, client: Client) -> None:
""" Save client """
raise NotImplementedError("save not implemented")
def update(self, client_id: str, client: Client) -> Client:
""" Update client """
raise NotImplementedError("update not implemented")
def delete(self, client_id: str) -> Client:
""" Delete client """
raise NotImplementedError("delete not implemented")repository/memory.py
This is the actual implementation of the Memory Repository. As a memory database we will be using a dictionary, since it’s implemented using hash tables that provides an average lookup complexity of O(1) [that’s the fastest you could get].
from typing import List
from .interface import IRepository
from domain.client import Client
class MemoryRepository(IRepository):
def __init__(self):
self._clients = {} # The actual memory database is a dictionary
def get(self, client_id: str) -> Client:
""" Get a specific client """
return self._clients.get(client_id)
def get_all(self) -> List[Client]:
""" Get all clients """
return list(self._clients.values())
def save(self, client: Client) -> None:
""" Save client """
if client.client_id in self._clients:
raise ValueError(
f"Client with ID {client.client_id} already exists!"
)
self._clients[client.client_id] = client
def update(self, client_id: str, client: Client) -> Client:
""" Update client """
if client_id not in self._clients:
raise ValueError(
f"Client with ID {client_id} does not exist!"
)
self._clients[client_id] = client
return self._clients[client_id]
def delete(self, client_id: str) -> Client:
""" Delete client """
if client_id not in self._clients:
raise ValueError(
f"Client with ID {client_id} does not exist!"
)
removed_client = self._clients[client_id]
del self._clients[client_id]
return removed_clientNote that if anything bad happens, we are not printing or doing anything related to UI here. We are just raising exceptions, that will be handled by our application UI. For better handling we can create custom exceptions like ClientNotFound, ClientAlreadyExists, etc that we can catch in UI and show different windows or messages, based on the exception type. I will write a full article about the Repository Pattern, with advanced tips and tricks, so if you are interested, follow me, or check my profile. It could be already there.
I have already done a full Repository Pattern Guide
If you want to learn more about Repositories and build great ones, check this guide.
service.py
Here we will implement our entire Business Logic. For now, we will implement some basic business logic like filtering, generating reports, etc.
Now it’s time to also use our Repository that we have already created for managing the data.
This is how a basic service should look like:
from typing import List
from domain.client import Client
from repository.memory import MemoryRepository
class Service:
def __init__(self, repository: MemoryRepository):
self._repository = repository
def add_client(self, client: Client) -> None:
""" Add a new client """
self._repository.save(client)
def get_client(self, client_id: str) -> Client:
""" Retrieve a client by ID """
return self._repository.get(client_id)
def get_all_clients(self) -> List[Client]:
""" Retrieve all clients """
return self._repository.get_all()
def update_client(self, client_id: str, client: Client) -> Client:
""" Update a client's details """
return self._repository.update(client_id, client)
def remove_client(self, client_id: str) -> None:
""" Remove a client by ID """
self._repository.delete(client_id)
def filter_by_name(self, name_substring: str) -> List[Client]:
""" Filter clients by a substring of their name """
return [
client for client in self._repository.get_all()
if name_substring.lower() in client.name.lower()
]
def generate_report(self) -> str:
""" Generate a simple report about the clients """
total_clients = len(self._repository.get_all())
report = f"Total clients: {total_clients}\n\n"
for client in self._repository.get_all():
report += f"{client.client_id}. {client.name} - {client.email}\n"
return reportKeep attention on how each method is called layer-by-layer, so if you want to create a new client from ui, you will call the service method add_client which will call the repository method save. This is called layers-of-isolations. This way when we made a change in one layer, it won’t affect other layers. Just think about it, if you want to store the users in a file, you just modify the repository, and it will reflect your change in the entire application without doing extra refactoring to any other components. Let’s say you maybe want to add some validation on objects, you just have to modify the add_client method from service and maybe add a function validate_client before calling self._repository.save(client).
No extra refactoring, no extra work, everything is perfect.
console.py
In this section lays our UI, the great benefit of using this architecture is that you can decouple front-end of your app from the backend. This is a console application, but you can transform this in a GUI App, you can also have multiple UIs or you can create a GUI for desktop and console look for servers, the app will run the same, or you can just provide both.
from service import Service
from domain.client import Client
class Console:
def __init__(self, service: Service):
self._service = service
self._menu_options = {
"1": ("Add Client", self._add_client),
"2": ("View Client", self._view_client),
"3": ("View All Clients", self._view_all_clients),
"4": ("Update Client", self._update_client),
"5": ("Delete Client", self._delete_client),
"6": ("Filter Clients by Name", self._filter_by_name),
"7": ("Generate Report", self._generate_report),
"0": ("Exit", None)
}
def _print_menu(self):
for key, (description, _) in self._menu_options.items():
print(f"{key}. {description}")
def _add_client(self):
client_id = int(input("Enter Client ID: "))
name = input("Enter Client Name: ")
email = input("Enter Client Email: ")
phone_number = input("Enter Client Phone Number: ")
client = Client(client_id, name, email, phone_number)
self._service.add_client(client)
print(f"Client {name} added successfully!")
def _view_client(self):
client_id = int(input("Enter Client ID to view: "))
client = self._service.get_client(client_id)
if client:
print(client.name, client.email, client.phone_number)
else:
print("Client not found!")
def _view_all_clients(self):
clients = self._service.get_all_clients()
for client in clients:
print(
client.client_id,
client.name,
client.email,
client.phone_number
)
def _update_client(self):
client_id = int(input("Enter Client ID to update: "))
client = self._service.get_client(client_id)
if client:
name = input(f"Enter new name ({client.name}): ")
email = input(f"Enter new email ({client.email}): ")
phone_number = input(
f"Enter new phone number ({client.phone_number}): "
)
client.name = name or client.name
client.email = email or client.email
client.phone_number = phone_number or client.phone_number
self._service.update_client(client_id, client)
print("Client updated successfully!")
else:
print("Client not found!")
def _delete_client(self):
client_id = int(input("Enter Client ID to delete: "))
self._service.remove_client(client_id)
print("Client removed successfully!")
def _filter_by_name(self):
name_substring = input("Enter substring to filter by name: ")
filtered_clients = self._service.filter_by_name(name_substring)
for client in filtered_clients:
print(client.client_id, client.name)
def _generate_report(self):
report = self._service.generate_report()
print(report)
def run(self):
while True:
self._print_menu()
choice = input("Choose an option: ")
if choice in self._menu_options:
_, action = self._menu_options[choice]
if action:
action()
else:
break
else:
print("Invalid choice!")
input("Press Enter to continue...")You can maybe implement this with a parameter which tells what kind of ui the user wants, or what kind of repository wants.
Take a look at this example:
python main.py --ui console --persistence filemain.py
This is where we create each layer and run the application. It’s small, easy to follow, and here maybe you can add a Factory Design Pattern that can create different kind of repository (maybe FileRepository which will store data to file, DatabaseRepository which will store data to a Database, etc). You have the ability to make changes without refactoring and that’s great!
from repository.memory import MemoryRepository
from service import Service
from console import Console
def main():
repo = MemoryRepository()
service = Service(repo)
ui = Console(service)
ui.run()
if __name__ == "__main__":
main()Take a look at this main.py taken from another project made by me years ago which can give you an ideea of other things you can do.
from repository.performer_repository import PerformerRepository
from repository.cache_performer_repository import CachePerformerRepository
from service import Service
from service import UserService
from service import PerformerService
from console.console import Console
from api.exceptions import AuthError
import argparse
import sys
import asyncio
import os
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--username', help='username of account', required=False)
parser.add_argument('-p', '--password', help='password of account', required=False)
args = vars(parser.parse_args())
# get username and password from environment
username_from_env = os.environ.get('USERNAME_APP')
password_from_env = os.environ.get('PASSWORD_APP')
if args['username'] is None and args['password'] is None:
if username_from_env is None or password_from_env is None:
print('Please enter username and password')
sys.exit(1)
else:
username = username_from_env
password = password_from_env
else:
username = args['username']
password = args['password']
user_service = UserService(username = username, password = password)
try:
user_service.login()
except AuthError as auth_error:
print(auth_error)
sys.exit(1)
performer_repository = PerformerRepository()
cached_performer_repo = CachePerformerRepository(performer_repository)
performer_service = PerformerService(
user_service=user_service,
performer_repo=cached_performer_repo
)
service = Service(
user_service=user_service,
performer_service=performer_service
)
console = Console(service)
asyncio.run(console.run())
main()Validator Pattern — Validate your data
This pattern is usually used within Layered Architecture at the service level. This is used just like Repository. You create the Validator, add the validator to the Service and before saving the data, you call the validate method from validator. I already made a guide on Validator Pattern, so if you want to achieve more using this architecture, check this guide. In the future i will also update this guide and add the validation layer. I made this guide before the validator pattern guide. I am also planning to create a more guides on Layered Architecture, since it’s a complex topic. If you want to read more about this topic follow me and subscribe to my newsletter.
Completations
Thank you for reading this guide!
Since it was a long guide, the next guides will be about Repository Pattern (already posted) where we will discuss more advanced things that we can do with this, about Validator Pattern (already posted), so we can add validations for all data, Decorator Pattern that will help us implementing various repositories, and we will take this application and make it look more like a production application. We will also discuss about testing within this architecture, so if you are interested in learning more about software development, software architecture, design patterns, follow me.
Github Code
The entire core of our application is available at Github here: https://github.com/alexplesoiu/LayeredArchitecture-Example







