Home > Posts Organizing backend code

Max Heinritz

Organizing backend code

I’ve come to prefer a backend codebase structure that involves six top-level directories, arranged in an ordered dependency hierarchy:

Code in app/ can depend on all other code; code in domain/ can depend on anything except app/; etc.

Dependency restrictions can be enforced with tools like eslint’s no-restricted-imports or ArchUnit for Java.

Let’s walk through these directories in detail, starting from the bottom up.

Common and isomophic code

I’ve included these two in the same section because the only difference between them is that isomorphic code gets synced to the frontend repo.

Examples of code in these layers include:

Common code could be easily bundled up and distributed as packages or libraries via npm, maven, etc. Indeed, reusing open source external packages instead of hand rolling common code is generally the best approach. But in every codebase I’ve worked in, we’ve ended up with some components that are library-like in nature but still specific to our particular application.

Common code is often domain-agnostic, but not always. For example, it might include enums for transportation mode, currency code, etc.

Common and isomorphic code should be stateless and unaware of the dependency-injection system.

Gateway code

Examples of gateway code include:

Generally these are instantiated in a dependency injection container and available for user throughout the application.

Similar to common code, gateway code is usually but not always domain-agnostic. For example, if you are working on a payments company, you might have a common/psp-clients directory that holds client libraries for various payment service providers.

Gateway code is almost always domain-_model_-agnostic. For example, the client libraries for the PSPs should be written in a way that conforms to the external PSP rather than the team’s own domain model.

System code

Examples of system code:

The difference between “system” and “common” is stateful vs stateless. When I say “stateful”, I mean the component “owns” persistent data store like PostgreSQL, Redis, Elasticsearch.

A Redis client library wrapper would live in “gateway”. A durable event system that uses the Redis client library to write messages for consumers to read in our database – that would be a system-level component.

Domain code

The contents of the domain directory will vary based on the application problem domain. For example:

One way to think about domain code is: if a concept is part of your domain model shared with business stakeholders, it should be within the domain directory. If a concept is exposed to end users in the product, it should live within the domain directory. End users do not care about which logging library you use (at the gateway layer) or which web server runner you use (at the app layer). End users do care about the domain concepts they manipulate through the product (at the domain layer).

For this reason, I tend to include in the domain directory any modules responsible for tenants, users, and roles. While these are domain-agnostic in the sense that any software system needs them, they are domain-model-specific in that the behavior of such modules heavily impacts product behavior and is an important part of users’ mental models of the system.

App code

The app directory is the orchestrator of the system. It is like the common directory in that it doesn’t deal directly with domain concepts, but it differs in that it depends on the domain code. Here are some examples of things that live at the app layer:

Unlike the common code, app code cannot simply be copied from one codebase to another. But the parts that reference the domain code can be built in a configurable way that is easy to swap out for other domains.

Tying it all together

A codebase for a restaurant SaaS application might look something like:






These buckets provide the high-level scaffolding for an application, and other techniques for code organization within these buckets merit their own descriptions.