-
Notifications
You must be signed in to change notification settings - Fork 653
Project Structure
When it comes to a project structure, an important rule is to think in terms of layering and not grouping. Layering will provide a clear hierarchy for rules and guidelines to exist. That being said, you can’t have an unlimited number of layers.
There have been several studies over the decades that have come to the same conclusion, the average person can’t maintain more than five things in their head at any given time. When you add things to the project that require a developer to keep more than five things in their head at any given time, you are setting them up for failure. Therefore, the project structure will maintain this rule of five.
Here are the five outermost layers of the project.
Project/
|-- api/
|-- app/
|-- business/
|-- foundation/
|-- vendor/
|-- zarf/
The name chosen for each layer is important because you want the visualization of the layers to sit properly when looking at the project. You also want names to be as obvious as possible to what code is contained in them. All of this will help developers maintain mental models of the codebase.
Here is a brief description of the outermost layers, their purpose, and what code is contained in them.
The api layer is responsible for code that directly helps with the startup, shutdown, and transport concerns of the project. It contains the entry point for all the applications being developed which can be services, frontends, and cli tooling. The api layer is also responsible for code related to the transport being used. For service that is HTTP. The code associated with the api layer should be constrained to the following things:
- Clean startup and shutdown of the application.
- Receive and validate external input via a unique set of App layer data models.
The app layer is responsible for code that directly helps with the application concerns of the project. It defines the external API based on a set of handlers using proper data models and not transport related types. It provides support for encoding and validation the api layer will use. The code associated with the app layer should be constrained to the following things:
- Define the encoding of external data via a set of data models.
- Provide support for external data validation.
- Call into the business layer to process the input.
- Return errors to the business layer for processing.
- Send positive path responses back to the external caller.
Layers can have layers, and in this project the app layer has three layers:
Project/
├─ app/
├─ frontends/
├─ services/
├─ tooling/
frontends: This is where the browser based UI for the admin system exists. This service is written in React/NextJS. Eventually a second UI will exist for the customer.
services: This is where the backend API services exist. The main service is called sales-api and the sidecar service is called metrics. It’s the sales-api service that we will be building in this book.
tooling: This is where the command-line apps exist. These are apps that help administer the system or help with maintaining, managing, and debugging the apps in the project.
The business layer is responsible for code that directly helps to solve the business problem or provides reusable code that is needed by multiple applications. Choosing to place a reusable package in business vs foundation requires understanding the constraints the package can live within or without. One such constraint is logging. If logging is a requirement, then the reusable package must live in the business layer.
All the packages that live in this layer can be accessed by the application layer and never the other way around. Imports always need to go down into the layers and never up. Imports inside the same layer are allowed, but must be reviewed.
Note: An import between packages is one of the strongest policies that can be established in the project. This decision must be made with care.
The code associated with the business layer should be constrained to the following things:
- Business APIs for CRUD and processing business logic.
- Storage APIs for CRUD and other database specific access.
- Database and transaction support.
- Authentication and Authorization.
- Observability.
- Application support such as middleware.
This layer has three layers:
Project/
├─ business/
├─ core/
├─ data/
├─ web/
core: This is where the core business packages exist. There is a package for every domain of data that needs to be managed and accessed. These packages can only import each other if there is a foreign data key relationship between the domains.
data: This is where the data management packages exist. There is a mix of packages that are specific to the project working with a relational database and some that help abstract data concepts like ordering, paging, and transactions.
web: This is where the web specific support packages exist. Things like authentication, authorization, metrics, and middleware. These are reusable by different web applications in the app layer.
This layer is responsible for providing support to the business and application layers. These packages could be used for any project that is solving any sort of business problem. These packages need to have the highest level of reusability, so these packages should not import each other and they should not log. Think of the packages in this layer as being the standard library for the project.
This layer has no other layers.
This layer is responsible for maintaining all of the 3rd party packages that are being used. I believe in vendoring all the code the project needs until it’s not reasonable to do any longer. We will use the go mod vendor command to manage this folder.
This layer is responsible for containing all of the necessary configuration needed to build and deploy the applications for this project. The name Zarf is an amazing word for this layer, since a zarf is a sleeve that wraps around a hot cup (or container) so a person won’t burn their hands. Most of the configuration we need to manage is container based.
This layer has three layers:
docker: This is where the docker files live for building the different services.
k8s: This is where all the kubernetes configuration lives for the different environments the services will be deployed.
keys: This is where a private key is maintained for auth support. A key like this would never live in the project, but to allow the dev environment to run unencumbered, the key is maintained here.
Contact Bill Kennedy at bill@ardanlabs.com if you are having issues getting the project running.