Monolithic Architecture
Table of Contents
Key Ideas Before We Dive In
In this chapter you will understand what a monolithic architecture is and when it makes sense to use it in backend development.
You will see:
- What a monolith looks like in practice
- How it is structured internally
- Why many beginners and even big companies start with a monolith
- Common problems that appear as a monolith grows
- How monoliths relate to microservices and modular monoliths (later chapters)
You already know what backends and REST APIs are from earlier chapters, so we will not repeat that. We will focus only on what is specific to monolithic architecture.
Key idea: A monolith is a single deployable application that contains all backend features in one codebase, one process, and usually one database.
What Is a Monolithic Architecture?
The basic definition
A monolithic architecture is a backend application where:
- All features live in one codebase
- The application usually runs as one process
- You deploy it as one unit (for example, one Docker image or one server app)
- It usually uses one main database for everything
Imagine an e-commerce backend that handles:
- User accounts
- Product catalog
- Shopping cart
- Orders
- Payments
- Admin panel
In a monolith, all of these are part of a single application. You might have different modules or packages inside the codebase, but they are compiled, run, and deployed together.
Simple visualization
Monolithic backend:
Client (browser / mobile)
|
v
[ Monolithic API ]
|
v
[ Database ]
Everything, like /login, /products, /cart, /orders, is handled by that single API application.
How a Monolith Looks in Code
Example project structure
Here is what a simple monolithic FastAPI project might look like:
myshop/
app/
__init__.py
main.py
auth/
__init__.py
routes.py
services.py
models.py
products/
__init__.py
routes.py
services.py
models.py
orders/
__init__.py
routes.py
services.py
models.py
admin/
__init__.py
routes.py
services.py
models.py
core/
config.py
database.py
security.py
tests/
test_auth.py
test_products.py
test_orders.py
requirements.txt
DockerfileThis is still a single application:
app/main.pycreates one FastAPI app- All feature modules (
auth,products,orders,admin) are imported into this one app - You build and deploy one Docker image
myshop:latest
You might have multiple Python modules, but from the outside, it is still one monolith.
One process, many features
In production, you might start several instances of the same monolith for load balancing, but each instance still contains all features:
+----------------------+
Client -->| Monolith instance 1 |
Client -->| Monolith instance 2 |
Client -->| Monolith instance 3 |
+----------------------+
|
v
[ Database ]This is still a monolithic architecture, just scaled horizontally.
Typical Monolithic Flow
Let us walk through a simple example: creating an order in a monolithic ecommerce backend.
- Client sends POST
/orderswith cart items. - The monolithic API receives the request in one app.
- Inside the same codebase, it:
- Validates the user session (auth module)
- Reads product data (products module)
- Calculates prices and discounts (orders module)
- Creates an order record in the database (orders module)
- Calls payment service code (payments module)
- Returns a response with the new order.
All these steps happen in one application process, often in one database transaction.
Pseudo code:
@router.post("/orders")
def create_order(order_input: OrderCreate, user: User = Depends(get_current_user)):
cart_items = get_cart_items(user.id) # carts module
products = product_repository.get_by_ids(cart_items) # products module
total = pricing_service.calculate_total(products) # orders module
payment_result = payment_gateway.charge(
user.payment_method,
amount=total
) # payments module
order = order_service.create_order(
user=user,
products=products,
payment=payment_result
) # orders module
return orderEven though we call different modules, it is still one application.
Advantages of Monolithic Architecture
Monoliths have several strong advantages, especially for beginners and small teams.
1. Simple to understand and start with
You only have:
- One main app to run, like
uvicorn app.main:app - One code repository
- One runtime environment
For a beginner, this is much easier than managing many small services, each with their own configs, deployments, and databases.
Example setup:
# Run the whole backend
uvicorn app.main:app --reloadEverything is up: auth, products, orders, admin.
2. Easy to develop locally
You can:
- Clone one repo
- Run one command to start the server
- Debug across modules inside one IDE session
No need to simulate many microservices or local queues.
Table: Local development comparison
| Aspect | Monolith | Microservices (later) |
|---|---|---|
| Repositories | 1 | Many |
| Services to run | 1 backend + maybe DB | Many backends + DB + queue + more |
| Debugging | Single process | Multiple processes / network hops |
| Initial complexity | Low | High |
3. Simple deployment
You typically deploy:
- One Docker image or one app executable
- One process type (like
web) - One main database
Example Docker Compose for a monolith:
version: "3.8"
services:
backend:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myshop
db:
image: postgres:16
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=myshop
You deploy backend and db. That is all.
4. Strong consistency is easier
Since everything is in one app and one database, it is easier to:
- Use database transactions that cover multiple features
- Keep data consistent
- Share business rules across modules
For example, a single transaction can update both orders and inventory tables:
with db_session() as session:
order = create_order(session, user, items)
reduce_inventory(session, items)
session.commit()No cross-service network calls are needed.
5. Good for small or early-stage products
When you are:
- A solo developer, or
- A very small team, or
- Building an MVP / prototype
Then a monolith is usually the best choice.
You can move fast and change requirements quickly because everything is in one place.
Disadvantages and Pain Points
As a monolith grows, certain problems tend to appear.
1. Tight coupling
Features can become tightly coupled, which means:
- Changing one module accidentally breaks another
- Modules know too much about each other’s internal details
- There are circular imports and dependencies
Example of tight coupling:
# orders/services.py
from app.products.services import get_product_discount
from app.auth.services import get_user_level
def calculate_order_price(user_id, product_ids):
user_level = get_user_level(user_id)
discounts = [get_product_discount(pid, user_level) for pid in product_ids]
...
If get_user_level changes or moves, many parts might break.
Over time you risk creating a "big ball of mud", where everything depends on everything else.
2. Harder to scale specific parts
You can scale a monolith horizontally by running more instances. But you cannot easily scale only the part that needs it.
Example:
/productstraffic is light/searchtraffic is very heavy
With a monolith, you may need to scale the entire thing just because one area is heavy. This can waste resources.
Microservices solve this by scaling only the specific service, but that brings other complexity (later chapter).
3. Slower deployments as the app grows
Since everything is packaged and deployed as one unit:
- A small change in a tiny feature can require rebuilding and redeploying the whole app
- Deployments can take longer because the app is big
- Risk of breaking many unrelated features if tests are weak
Example scenario:
- You only modify the
adminmodule - But you still:
- Rebuild the Docker image for the entire backend
- Run all tests against the whole system
- Restart the entire backend process
4. Technology lock-in
In a monolith, all modules usually share:
- The same programming language
- The same framework
- The same database type
If you want to introduce a new language or framework, it is much harder than in a microservices architecture.
Example:
- You want to write a high-performance recommendation engine in Rust
- In a monolith, integrating that directly is complicated
- In microservices, you could run a separate recommendation service
5. Codebase complexity
Over time, if not structured well:
- The codebase becomes very large
- Onboarding new developers is slow
- It becomes hard to find where a particular feature lives
- Merge conflicts become common
This is where concepts like modular monoliths and layered architecture help by organizing a monolith properly. You will see those in separate chapters.
Monolith vs Microservices at a Glance
You will learn microservices later in detail. For now, compare the high-level differences.
| Aspect | Monolith | Microservices |
|---|---|---|
| Deployment unit | One app | Many small services |
| Data storage | Usually one main DB | Many databases or schemas possible |
| Initial complexity | Low | High |
| Team size fit | Small teams / early stage | Larger teams / complex orgs |
| Scaling | Whole app | Per service |
| Tech stack | Mostly uniform | Different stacks per service possible |
| Dev environment | Simple | Complex (many services to run) |
| Failure isolation | Low | Higher (a service can fail independently) |
Rule of thumb: Start with a monolith, structure it well, and only consider microservices when you clearly need them and have the experience and tooling to handle the extra complexity.
Common Misconceptions About Monoliths
Misconception 1: “Monoliths are always bad”
Reality:
- Many successful companies run well-architected monoliths
- Monoliths can be clean, modular, and maintainable
- A bad microservices design can be worse than a good monolith
The problem is not "monolith" itself, but poor internal architecture.
Misconception 2: “Monoliths cannot scale”
Reality:
- You can scale a monolith horizontally, for example, by running more containers behind a load balancer
- You can optimize performance with caching, database tuning, and CDNs
- For many applications, a well-scaled monolith is enough for a long time
Scaling example with Docker:
# Scale monolith to 4 containers (depending on your tool)
docker compose up --scale backend=4Misconception 3: “Using multiple modules means microservices”
Multiple modules or packages in the same codebase do not make it microservices. Microservices are separate deployable services that communicate over the network.
If everything is:
- Built together
- Deployed together
- Runs inside the same process type
then it is still a monolith, even if internally it is modular.
When Should You Choose a Monolithic Architecture?
Monoliths are usually the right choice when:
- You are building your first backend project
- You are working on a small to medium app
- You have a small team
- You are building an MVP prototype to validate an idea
- You do not yet have very different performance or scaling needs across features
Some good fit examples:
- Task management API
- Simple e-commerce backend
- Internal company tools
- Blogging platform
- Early version of a SaaS product
In later chapters, you will see how to structure a monolith well:
- Layered architecture to separate web, service, and data layers
- Service layer to keep business logic in one place
- Repository pattern to isolate database access
- Modular monoliths to keep domains separated inside a single app
Typical Monolithic Example in Practice
Let us put the concepts into a very small example. Imagine this simple FastAPI monolithic app:
# app/main.py
from fastapi import FastAPI
from app.auth.routes import router as auth_router
from app.todos.routes import router as todos_router
app = FastAPI()
app.include_router(auth_router, prefix="/auth")
app.include_router(todos_router, prefix="/todos")Authentication module:
# app/auth/routes.py
from fastapi import APIRouter, Depends
from .schemas import UserCreate, UserOut
from .services import register_user, get_current_user
router = APIRouter()
@router.post("/register", response_model=UserOut)
def register(user: UserCreate):
return register_user(user)
@router.get("/me", response_model=UserOut)
def me(current_user = Depends(get_current_user)):
return current_userTodos module:
# app/todos/routes.py
from fastapi import APIRouter, Depends
from .schemas import TodoCreate, TodoOut
from .services import create_todo, list_todos
from app.auth.services import get_current_user
router = APIRouter()
@router.post("/", response_model=TodoOut)
def create(todo: TodoCreate, user = Depends(get_current_user)):
return create_todo(user.id, todo)
@router.get("/", response_model=list[TodoOut])
def all(user = Depends(get_current_user)):
return list_todos(user.id)Everything is:
- In one project
- Started using one command
- Deployed as one app
That is a simple monolithic backend.
How Monoliths Evolve
A realistic path for many backends:
- Simple monolith
- A single codebase, minimal structure, all logic in routes and a few helpers.
- Better-structured monolith
- Separate layers: routes, services, repositories
- Separate modules for different domains: auth, products, orders, etc.
- Modular monolith (later chapter)
- Strong boundaries between modules
- Clear interfaces between domains
- Limited cross-module dependencies
- Partial extraction to services
- Some parts that need independent scaling or technology changes may be moved into separate services if really needed (microservices, event-driven architecture, etc).
You should focus on steps 1 to 3 first. Jumping directly to microservices often creates unnecessary complexity.
Summary
- A monolithic architecture is a single deployable backend that contains all features in one application and usually one database.
- It is simpler to build, run, and deploy, which makes it ideal for beginners, small teams, and MVPs.
- Monoliths can scale and can be clean and modular if you use proper internal architecture.
- Main challenges appear as the codebase and team grow, such as tight coupling, harder scaling of specific parts, and longer deployments.
- You will see modular monoliths, layered architecture, and service layer patterns later, which are ways to keep a monolith maintainable.
In the next chapters on architecture, you will compare monoliths with modular monoliths and microservices and learn how to choose and design the right architecture for your backend.
Views: 6
KAHIBARO