Folder structure and file naming conventions

Decision

We adopt the following conventions for file and folder naming in our project:

  • File naming: The filename and -casing is the same as the name of the main export of the file. When a class is the main export, we will use PascalCase filename, if it's a method, we use camelCase.

  • No Extensions in File Names: We do not use extensions like .repository.ts; instead, files are named after their main export, where the exported instance does not need to have its type in its name. For example, a UserRepository class would be named UserRepository.ts, but a Button component can simply be Button.tsx as well.

  • Test files: For our test files, we keep the .test.(ts,js) extension. Our test files are using the same file name as the file under test.

  • Special case, naming of lambda handlers: Since the main export of lambda handlers would always lead to filenames being handler.ts which does not tell anything about the functionality, we apply a special file naming convention to those files. Therefore, we use <StackName>.<LambdaFunctionId>.ts so that we don't have to specify an entry or handler property in CDK code as well.

  • camelCase for Folder Names: We use camelCase for folder names, except when there is a barrel file inside and the main export is with PascalCase.

  • Bundle Domain Logic: Instead of grouping files by "type", we bundle domain logic together. This means that within a project, domains are organized as individual folders, facilitating focused work within domain boundaries.

  • Clear Domain Representation: The src folder should reflect the domains for which the application is responsible. Each domain is represented by a folder directly under src.

  • Integration of CDK Infrastructure: When the project contains AWS CDK (Cloud Development Kit) infrastructure, we place the stacks/constructs at the same level as the corresponding use cases, providing a clear view of infrastructure dependencies.

  • Absence of "domains" Folders: We do not use a separate domains folder. Instead, each domain has its own folder directly under src.

  • Adapters and use case organization: The use cases are located in the root of a domain folder. As written in the Hexagonal architecture: Adapters learning journey, the adapter files live next to the use case where they are used because it's usually a 1:1 relation between those two.

  • Framework-Specific Code Organization: App-wide server-framework-specific code is kept within a specific folder in src, ensuring a clear separation of concerns. To differ between app wide code and domain logic, we use underscore patterns, e.g. src/_http.

  • Migration Strategy: When creating a new endpoint or job, or modifying an existing one, we migrate the logic into a domain folder. While domains might not be fully defined, this approach helps in identifying and managing highly coupled domains within our existing applications.

An example project could look like this:

supplier-manager
│   README.md
│
└───src
    │   SupplierManagerStack.ts
    │   SupplierManagerStack.RequestAuthorizerHandler.test.ts
    │   SupplierManagerStack.RequestAuthorizerHandler.ts
    │
    └─── sendSupplierFiles
    │   │   
    │   └──── campaign
    │   │    │   Campaign.ts
    │   │    │   CampaignRepository.ts
    │   │    │   InMemoryCampaignRepository.ts
    │   │    │   HermioneCampaignRepository.test.ts
    │   │    │   HermioneCampaignRepository.ts
    │   │
    │   └──── supplierServer
    │   │    │   SupplierServer.ts
    │   │    │   SupplierServerRepository.ts
    │   │    │   InMemorySupplierServerRepository.ts
    │   │    │   ...
    │   │
    │   │   SendSupplierFilesStack.ts
    │   │   SendSupplierFilesUseCase.test.ts
    │   │   SendSupplierFilesUseCase.ts
    │   │   SupplierAdapter.ts
    │   │   ...
    │   
    │
    └─── ... other domains with use cases

Problems

Before adopting these conventions, our codebase lacked consistency and clarity in file and folder organization across teams. Developers found it challenging to locate code related to specific domains, and there was ambiguity regarding where to place framework-specific code. Additionally, inconsistent file naming practices led to confusion and made it difficult to understand the business purpose of files at a glance. In many cases there was no clear separation of business logic and framework specific code.

There is no clear understanding of the domains that exist and where their boundaries lie.

Context

Each of the product teams is applying different folder and file naming conventions even when working on the same projects. There is no clear guideline defined which makes it easy to know where to find what across the different projects.

CTO ran a workshop in February 2024 on how to write modular code.

When looking at the src folder of a project / module, usually you can't recognize what this part of the monorepo is responsible for. Business logic is mixed with framework specific code. In most cases, but especially in Filch and Hermione, the project is organized based on "type" and not based on business domains.

At the moment of writing this ADR, we haven't defined clear domains and domain boundaries. Especially campaigns and automations are used throughout all of our projects without making it clear what they consist of the context of each service. Managing team responsibilities within Hermione and Filch therefore is quite challenging.

Options

The following options for file naming conventions were considered

  1. Use custom extensions with kebab-case filenames
  2. Use custom extensions with camelCase filenames
  3. Always use camelCase or PascalCase without custom extension based on the main export
  4. Always use camelCase or PascalCase with custom extension based on the main export
  5. camelCased file naming based on type or implementation

Regarding folder naming conventions and general project organization, we mainly considered if we should put the individual domains into a domains folder or not.

Reasoning

We chose the adopted file naming convention by a majority voting via slack after collecting advantages and disadvantages of each option on a Miro board.

The selected conventions promote clarity, consistency, and maintainability by providing a clear structure for organizing code and ensuring that file and folder names accurately reflect their contents. By bundling domain logic, adopting clear naming conventions, and aligning with CDK infrastructure, we aim to streamline development workflows and facilitate collaboration among team members but also across teams. Splitting our codebase into domain folders will also help us to identify and define proper domains and their boundaries.

Consequences

How do we implement this change?

We won't adapt all of our files and projects at once after accepting this ADR.

Each team will use those conventions when working within the monorepo. Applying it should be simple due to the clear guidelines.

When creating a new endpoint or job, or modifying an existing one, we migrate the logic into a domain folder. In the transition phase there will be a mixture of old and new conventions within the projects which is an accepted tradeoff.

For example, inside Hermione project it could look like this when controlAddresses domain is already refactored but the rest isn't:

shared/monorepo/apps/application
│   README.md
│
└───src
    │   index.ts
    │   app-cluster.js
    │
    └─── controlAddresses
    │   │   
    │   └──── controlAddress
    │   │    │   ControlAddress.ts
    │   │    │   ControlAddressRepository.ts
    │   │    │   InMemoryControlAddressRepository.ts
    │   │    │   ApplicationDatabaseControlAddressRepository.test.ts
    │   │    │   ApplicationDatabaseControlAddressRepository.ts
    │   │
    │   │   controlAddressRoutes.ts
    │   │   CreateControllAddressUseCase.test.ts
    │   │   CreateControllAddressUseCase.ts
    │   │   GetControllAddressUseCase.test.ts
    │   │   GetControllAddressUseCase.ts
    │   │   ...
    │   
    │
    └─── _http
    │   │   
    │   └──── ... lib folder should be renamed to "_http" and only contain logic which 
    │             is specific to the used framework (currently: restify)
    │
    └─── roles
    │   │   
    │   └──── hermione -> everything written the "old" way lives here

Who will implement the change?

Everyone inside product will follow those conventions.

How do we teach this change?

Besides this ADR, the enable team will run a short workshop to show an example on how refactoring our existing projects would look like. Then people can team up to "clean up" a part of their codebase that they are working on. The goal of the workshop is to merge the refactoring to main.

What could go wrong?

Unintentional failure to follow conventions can occur out of habit. Furthermore, unclear domain boundaries could lead to misplaced code.

What do we do if something goes wrong?

Any time we become aware that we have unfortunately not followed the conventions, we take corrective action and adjust it in a pull request.

When recognizing that code was misplaced due to unclear domain boundaries, the respective team should get in touch with the enable team to define proper domain boundaries. Depending on the project we might need to move some code to another domain inside the same project or get in touch with other teams if this part is in their responsibility.

What is still unclear?

There might be the chance to enforce at least some of the conventions via our eslint setup. The enable team will follow up on this, as adapting it while still being in a transition phase will lead to a lot of warnings and errors. Also, as a first step, we would need to check how this influences eslint run times.

Related ADRs