Layered Architecture for Constructing Readable, Robust, and Extensible Apps

-

: your code works but confidence is low, so that you hesitate to the touch it. Adding a feature means performing open-heart surgery on the appliance, modifying existing business logic reasonably than extending the system. Over time, the fee of change keeps rising.

Does this feel familiar?

  • Changes feel dangerous since you fear modifying the code might trigger unintended negative effects.
  • You spend a whole lot of time scrolling through large files, finding or understanding code.
  • You will have functions that “do every thing” and have 10+ parameters.
  • Tests are skipped or require spinning up a database, manually preparing records and cleansing up afterwards.
  • FastAPI routes that construct SQL queries.

The applying should still be delivering value, nevertheless it feels brittle. Structure is unclear, responsibilities are blurred, and small changes feel disproportionately expensive.

If this resonates, this post is for you.


TL;DR

  • If adding features feels dangerous or slow, the issue is usually structure, not code quality
  • Layered architecture separates responsibilities and keeps business logic independent of frameworks and infrastructure
  • Vertical slicing by domain prevents layers from turning into dumping grounds as systems grow
  • Application layers orchestrate workflows; domain layers define meaning and constraints
  • Clear boundaries reduce cognitive load, improve testability, and make change cheaper over time

Good structure doesn’t add ceremony. It preserves momentum.


The goal of this text

Debating whether a bit of code belongs in a single layer or one other misses the purpose. The goal shouldn’t be perfect categorization but…

to architect an application with principles of loosely coupled layers of responsibility that make the system easier to understand, test and evolve.

We aim for applications which might be:

  • Readable: easy to navigate and reason about, with low cognitive load
  • Robust: failures are contained, predictable, and comprehensible
  • Extensible: latest functionality is added by extension, not by rewriting existing logic. Existing components are loosely coupled, modular and replaceable.

To get there we’re going to structure the app into layers, each with a transparent responsibility. This separation and the best way layers relate to one another allows the system to evolve over time.


Disclaimer:

This shouldn’t be the most effective or only method to structure an application and it’s not a one-size-fits-all solution.

What follows is a technique I arrived at by refining it over several years across different projects. I took inspiration from DDD, SOLID principles, onion, hexagonal and layered architecture but combined every thing into something that works for the sorts of systems I typically construct.


The Layers

Within the image below you’ll find an summary of the layered architecture:

Layer relationships (image by creator)

Before diving into the responsibilities of every layer, it helps to first understand how they relate to one another.

Layer Relationships (inward flowing dependencies)

The outer layer will be split in two sides:

  • Input side
    The interface layer, which receives data and acts because the entry point of the app
  • Output side:
    The repository and infrastructure layers, which can communicate with external systems resembling API, database and message queues

The interface layer calls the appliance layer, which is the core of the system, where business logic lives. The applying layer, in turn, calls into the repository and infra layers to persist data or communicate externally.

An important take-away is that dependencies flow inward.

Business logic doesn’t rely on frameworks, database or transport mechanisms. By isolating business logic, we gain clarity and make testing significantly easier.


Domain layer

The domain layer primarily focuses on constraints, not orchestration or negative effects. It comprises definitions that ought to reflect business meaning and needs to be comprehensible by business people. Take into consideration dataclasses or Pydantic models.

These models define the form and constraints of the information flowing through the system. They needs to be strict and fail early when assumptions are violated. Heavy validation ensures prime quality data within the core of our system.

A useful side effect is that domain models turn into a shared language. Non-technical stakeholders may not read the code line-by-line but they’ll often understand the structure and intent.


Application layer

That is the center of the system.

The app layer is accountable for orchestrating business logic and workflows. It may get called from the interface layer and coordinates domain models, repositories and infrastructure services to attain a particular business consequence. As well as it’s accountable for handling application-level failures while keeping domain and infrastructure concerns isolated.

A great rule of thumb: Should you can unit-test this layer without spinning up a database or web server, you might be on the proper track.


Infrastructure layer

This layer comprises anything that supports the app but comprises no business logic. Consider this layer as “tools for the app layer”; it only must know to call, not it’s implemented. For instance, it should find a way to call send_email(...) without knowing anything about SMTP configuration.

By decoupling these concerns you localize complexity and make integrations easier to switch, upgrade or debug.

Examples:

  • logging setup
  • hashing and crypto utilities
  • http clients
  • message queue clients
  • email senders

Interface layer

The interface layer is how the surface world talks to your system and will act as a gateway for proper data. Consider an API, CLI, queue consumer or something that a CRON job can call.

I keep these layers thin and void of business logic. I aim for just just a few responsibilities:

  1. Receiving input
  2. Validating and normalizing (transport-level) input (types, format e.g.)
  3. Calling the appliance layer
  4. Formatting the response

Repository layer

The repository layer defines persistence boundaries (e.g. communication with a database). The aim is to decouple your application/business logic from a selected database implementation. This includes ORM models, database schemas, SQL queries, and persistence-related transformations.

The applying layer shouldn’t be accountable for:

  • Which database you utilize
  • How queries are written
  • Whether data comes from SQL, a cache or one other service

The app layer should find a way to simply call e.g. get_customer_id(customer_id) and receive a site object in return. This separation really pays of when it’s essential to switch database, move persistence behind an API, add caching or need to test and not using a real database.

a very large building that has been destroyed
Is your application ready for change? (Photo by Jade Koroliuk / Unsplash)

Methods to start layering?

It’s pretty easy to start. You don’t even should refactor your whole app instantly. It may be so simple as just 5 folders in your src folder at the basis of your project:

- src/
  - application/
    - core.py
  - domain/
    - customer.py
  - infrastructure/
    - weather_api_client.py
  - interface/
    - api/
      - (files that contain FastAPI or Flask e.g.)
    - cli/
    - web/
      - (files for a streamlit app e.g.
  - repository/
    - schemas.py
    - customer_repo.py

Remember: the goal is to pedantically categorize each bit of code in a file and call it a day; the separate files and folders should reflect the incontrovertible fact that your system is layered and decoupled.


Larger apps: Horizontal layering per domain boundary

The instance above shows a fairly small application that’s layered horizontally only. This works well at first, but larger projects can quickly collapse into “God-modules”.

Engineers are smart and can take shortcuts under time pressure. To avoid your layers becoming dumping grounds, you need to explicitly add vertical slicing by domain.

Horizontal layering improves structure; vertical slicing by domain improves scalability.

The foundations usually are not about restriction or purity but act as guard rails to preserve architectural intent over time and keep the system comprehensible because it grows.

a pile of rocks sitting on top of a mountain
A small application with 3 layers (Photo by Oghenevwede Okuma / Unsplash)

Applying horizontal and vertical layers

In practice, this implies splitting your application by domain first, after which layering each domain.

The app in the instance below has two domain: subscriptions and users that are each sliced into layers.

src/
  application/                    <-- that is the composition root (wiring)
    fundamental.py
    
  subscriptions/                  <-- it is a domain
    domain/
      subscription.py
      cancellation_reason.py
    application/
      cancel_subscription.py
    repository/
      subscription_repo.py
    infrastructure/
      subscription_api_client.py
    interface/
      api.py

  users/                           <-- one other domain
    domain/
    application/
    repository/
    interface/

Within the structure above fundamental.py is the composition root which imports and calls functions from the appliance layer within the subscriptions and users domains and connects them to infrastructure and interfaces. This dependency flows inward, keeping the domains themselves independent.

Core rules

Layering and domain boundaries give our app structure but without some basic rules the architecture collapses quietly. Without rules the codebase slowly drifts back to hidden coupling, circular dependencies and domain logic leaking across boundaries.

To preserve structure over time I exploit three rules. These rules are intentionally easy. Their value comes from consistent application, not strict enforcement:

Rule 1: Domains don't import one another’s internals.
If subscriptions imports users.domain.User directly you possibly can not change users without affecting subscriptions. Since you lose clear ownership, this makes testing this domain in isolation so much harder.

  • Move truly shared concepts right into a shared domain or
  • pass data explicitly via interfaces or DTO’s (often as IDs reasonably than objects)

Rule 2: Shared concepts go in a shared domain
This makes coupling explicit and intentional to avoid “shared” things getting duplicated inconsistently or worse: one domain silently becomes the “core” every thing will depend on.

  • keep the domain small and stable
  • it should change slowly
  • it should contain abstractions and shared types, not workflows

Rule 3: Dependencies flow inward inside each slice
This keeps business logic independent of delivery and infrastructure.

You’ll notice when this rule is broken when domain or application code starts depending on FastAPI or a database, test will turn into slow and brittle and framework upgrades ripple through the codebase.

Keep dependencies flowing inward to be certain that:

  • You possibly can swap interfaces and infrastructure
  • You possibly can test core logic in isolation
  • Your small business logic survives change at the perimeters
low angle photography of high rise building under white clouds during daytime
Tall buildings required a well-considered architecture (Photo by Clay LeConey / Unsplash)

Practical example: refactoring an actual endpoint

For instance the advantages, consider an endpoint that cancels a magazine subscription and returns alternative suggestions.

The initial implementation put every thing in a single FastAPI endpoint:

  • Raw SQL
  • Direct calls to external APIs
  • Business logic embedded within the HTTP handler

The code worked, nevertheless it was tightly coupled and hard to check. Any test required an online server, an actual database, and extensive setup and cleanup.

Refactored design

We refactored the endpoint by separating responsibilities across layers.

  • Interface layer
    API route that validates input, calls the appliance function, maps exceptions to HTTP responses.
  • Application layer
    Orchestrates the cancellation workflow, coordinates repositories and external services, and raises use-case level errors.
  • Repository layer
    Centralizes database access, easy functions like get_user_email(user_id).
  • Infrastructure layer
    Incorporates API clients for external SubscriptionAPI and SuggestionAPI, isolated from business logic.
  • Domain layer
    Defines core concepts resembling User and Subscription using strict models.

Result

The endpoint became a skinny adapter as an alternative of a God-function. Business logic can now be tested without spinning up an API server or a database. Infrastructure is replaceable and the code-base is more readable.

Change is less expensive; latest features are built by adding latest code as an alternative of rewriting existing logic. Recent engineers ramp up faster on account of reduced cognitive load. This makes for a much more robust app that may safely evolve.


Conclusion

Layered design shouldn't be about adding ceremony or chasing a textbook ideal. It’s about ensuring your system stays comprehensible and adaptable because it grows.

By separating responsibilities and keeping layers loosely coupled, we reduce the cognitive load of navigating the codebase. This makes failures easier to isolate, and allows latest functionality to be added by extension reasonably than by rewriting existing logic.

These advantages compound over time. Early on, this structure might feel like double work or unnecessary overhead. But as complexity increases the payoff becomes clear: changes turn into safer, testing becomes cheaper and teams move faster with greater confidence. The system stays stable while interfaces, infrastructure and requirements are capable of change around it.

Ultimately, a well-layered application makes change cheaper. And in the long term, that’s what keeps software useful.


I hope this text was as clear as I intended it to be but when this shouldn't be the case please let me know what I can do to make clear further. Within the meantime, take a look at my other articles on all types of programming-related topics.

Comfortable coding!

— Mike

P.s: like what I’m doing? Follow me!

ASK ANA

What are your thoughts on this topic?
Let us know in the comments below.

0 0 votes
Article Rating
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Share this article

Recent posts

0
Would love your thoughts, please comment.x
()
x