Software architecture is the skeleton of any application. It determines how components interact, how code evolves over time, and how much it will cost to maintain two years from now. Pick the wrong one, and it becomes a drag.
What is software architecture?
Software architecture describes how a system is organized: how the code is split up, how components communicate, which principles guide design decisions, and above all which trade-offs you accept along the way.
It isn't limited to the functional side (what the application does). It also covers everything that gets measured once the application is in production: performance, security, scalability, and maintainability. Software can do exactly what it's supposed to do and still be impossible to maintain six months later.
Monolithic architecture
What is it?
Monolithic architecture is the traditional approach to software development. The entire application is developed, deployed, and maintained as a single unit. All components (user interface, business logic, data access) are grouped in the same project.
# Example of a monolithic Flask application
from flask import Flask, render_template
from models import User, Order
from services import PaymentService, EmailService
app = Flask(__name__)
@app.route('/order', methods=['POST'])
def create_order():
# Everything runs in the same process
user = User.get_current()
order = Order.create(user)
PaymentService.process(order)
EmailService.send_confirmation(user, order)
return render_template('order_success.html')Advantages
One project to manage, one deployment, and all the code in one place when you need to debug. No network latency between components either, and less infrastructure to set up, so a lower initial cost.
Limitations
You can't scale only part of the application. A minor change means redeploying everything, and since it's all tightly coupled, a modification can have unforeseen side effects. Over time, the code becomes hard to maintain.
When it's the right choice
For a startup still in validation phase or an MVP that has to ship fast, the monolith is almost always the right answer. As long as you're still looking for the right product, you don't need distributed infrastructure; you need short feedback loops. Internal enterprise apps with a small dev team follow the same logic.
Microservices architecture
What is it?
Microservices architecture breaks down the application into independent services, each responsible for a specific business function. These services communicate with each other via APIs, typically REST or through asynchronous messages.
# Docker Compose example for microservices architecture
version: '3.8'
services:
user-service:
image: myapp/user-service
ports:
- "8001:8000"
order-service:
image: myapp/order-service
ports:
- "8002:8000"
depends_on:
- user-service
payment-service:
image: myapp/payment-service
ports:
- "8003:8000"
notification-service:
image: myapp/notification-service
ports:
- "8004:8000"Advantages
- Each service can be scaled independently
- One service failing doesn't impact the others
- Each service can use its own stack
- Deployments are independent, so updates are more frequent and less risky
- A team can manage its own service
Limitations
Monitoring, logging, and debugging all become distributed, and heavier for it. Calls between services add latency, distributed transactions make data consistency complex to guarantee, and you need more infrastructure resources.
When it's the right choice
Microservices come into their own on high-traffic platforms (e-commerce, SaaS, social networks) and in organizations where several teams work in parallel on different parts of the product. Netflix, Amazon, and Spotify use them for that reason. Going microservices with three developers and no traffic, on the other hand, is almost always a mistake: you inherit the complexity without the benefits.
Hexagonal architecture (Ports & Adapters)
What is it?
Hexagonal architecture, also called "Ports and Adapters", places the business domain at the center of the application. Business code is isolated from technical concerns (database, frameworks, interfaces) through interfaces (ports) and implementations (adapters).
# Hexagonal architecture in Python
# The port (interface) - domain side
from abc import ABC, abstractmethod
class OrderRepository(ABC):
@abstractmethod
def save(self, order: Order) -> None:
pass
@abstractmethod
def find_by_id(self, order_id: str) -> Order:
pass
# The business domain - at the center of the hexagon
class OrderService:
def __init__(self, repository: OrderRepository):
self.repository = repository
def create_order(self, items: list) -> Order:
order = Order(items)
order.validate() # Pure business logic
self.repository.save(order)
return order
# The adapter - infrastructure side
class PostgresOrderRepository(OrderRepository):
def save(self, order: Order) -> None:
# PostgreSQL-specific implementation
self.db.execute("INSERT INTO orders ...")
def find_by_id(self, order_id: str) -> Order:
result = self.db.execute("SELECT * FROM orders WHERE id = %s", order_id)
return Order.from_dict(result)Advantages
The business domain can be tested without external dependencies, and you can change databases without touching business logic. The separation between business and technical logic is clean, and adding a new adapter takes almost nothing.
Limitations
- You need a good understanding of SOLID principles to get started
- More code to write, between the interfaces and the implementations
- Quickly excessive on simple projects
When it's the right choice
As soon as business logic is central and likely to evolve, hexagonal architecture is worth the investment. That's typically the case for custom enterprise software with business rules that change depending on the contract, the client, or the regulation. If you practice Domain-Driven Design or if unit testing is a stated priority, you'll end up there naturally.
Layered architecture
What is it?
Layered architecture organizes the application into horizontal strata, each with a specific responsibility. Typically, you'll find three or four layers: presentation (user interface), business logic (services), data access (repositories), and sometimes an infrastructure layer. Each layer only communicates with adjacent layers, creating a clear hierarchy.
- Presentation layer: controllers, views
- Business layer: services, use cases
- Data layer: repositories, DAOs
- Infrastructure layer: database, external APIs
Advantages
- Every developer knows where to place their code
- Layers can be reused in other projects
- Modifications stay localized to a specific layer
- New developers quickly understand the structure
Limitations
Changes often traverse all the layers, and a business modification may require adjustments at every level. Successive calls between layers can also impact performance.
When it's the right choice
For a traditional CRUD application, or a project where the team is structured by technical skill (frontend, backend, DBA), splitting into layers matches how the organization actually works. It's also the most widely taught architecture, so the most natural one for junior teams starting out.
Event-driven architecture
What is it?
Event-Driven Architecture (EDA) relies on the production, detection, and reaction to events. Instead of synchronous calls between components, services emit events that other services can consume asynchronously.
# Simplified event-driven architecture example
# Event producer
class OrderService:
def __init__(self, event_bus):
self.event_bus = event_bus
def create_order(self, order_data):
order = Order.create(order_data)
# Emits an event instead of directly calling other services
self.event_bus.publish("order.created", {
"order_id": order.id,
"user_id": order.user_id,
"total": order.total
})
return order
# Event consumers
class NotificationService:
@event_handler("order.created")
def on_order_created(self, event):
user = User.find(event["user_id"])
self.send_email(user.email, "Your order has been created!")
class InventoryService:
@event_handler("order.created")
def on_order_created(self, event):
self.reserve_stock(event["order_id"])Advantages
Services don't know each other directly, which decouples them strongly. Consumers can be scaled independently, events can be replayed in case of failure, and you add new consumers without modifying the producer.
Limitations
- Tracing a flow through multiple events is difficult
- No guarantee of immediate data consistency (eventual consistency)
- You need a message broker (RabbitMQ, Kafka, etc.)
When it's the right choice
Real-time systems (trading, IoT, notifications) are natural candidates. Same logic for workflows where one action triggers others in cascade: order validated → stock reserved → invoice generated → email sent. As long as the steps are independent, putting an event bus in the middle simplifies a lot of things.
How to choose the right architecture?
The choice depends on your context:
| Criteria | Monolithic | Microservices | Hexagonal | Event-Driven |
|---|---|---|---|---|
| Team size | Small (< 10) | Large (10+) | Medium | Medium to large |
| Business complexity | Low | Variable | High | Variable |
| Initial budget | Limited | Significant | Moderate | Significant |
| Scalability needs | Low | High | Moderate | High |
| Time-to-market | Short | Long | Medium | Long |
| Real-time | No | Possible | No | Yes |
A few principles that apply regardless of the project:
-
Start simple. A well-structured monolith can evolve into microservices if the need arrives. The other way round, much less so. As long as the complexity isn't justified by a real problem, it's just a cost.
-
Consider your team. A sophisticated architecture dropped onto a team that doesn't master it will generate more bugs than benefits. The architecture has to match the skills available, not the other way round.
-
Combine approaches. Nothing stops you from running a hexagonal architecture inside a monolith, or mixing event-driven and microservices. The categories in an article like this one are tidier than the reality of projects.
-
Plan for evolution. If you start monolithic, identify from the outset the functional boundaries that might one day become separate services. It costs almost nothing to write and it makes what comes next much easier.
Further reading
- Software architecture on Wikipedia: overview of the concepts
- Microservices.io: patterns and best practices for microservices
- The Twelve-Factor App: methodology for building modern applications
Whatever you pick, the right reflex is to keep a written record of your architecture decisions and the reasons behind them. It saves you from having to guess, two years later, why the order service talks to the payment service asynchronously.



