Naming code is hard enough, choosing the right names for folders can be downright stressful.
I can think of a dozen project where folder structure was an afterthought and code was all over the place.
I've found a simple 2D matrix that helps to classify the purpose of code, and make grouping files a much simpler problem.
| Abstract | Concrete |
---|
Transport | ? | ? |
---|
Logic | ? | ? |
---|
Service | ? | ? |
---|
## y: Transport / Logic / Service
The first step is to consider your project as series of steps between an agent and some data.
The agent performs an action, some logic is applied, data is retrieved or manipulated.
Regardless of who the agent is - human, bot, event - or what part of the tech-stack, most programs follow this pattern.
The y axis of this matrix comes from the three areas of concern found in this flow.
Agent --> Transport --> Logic --> Service --> Data
Transport Layer is all about moving and choosing. Be it routes, pages, or event handlers, a transport file lets
the agent make a choice and direct the program to execute a certain function.
Logic Layer is where the meat of your app resides. These are the functions that determine the shape and rules of your interaction: What is allowed and what isn't.
Service Layer is where you talk to third-party code. No program stands alone and at some point you'll need to interact with another service. Be it a database, a rest api, or an AWS resource,
the service layer handles code that talks to other systems.
@note: You might be thinking that this pattern is pretty similar to MVC, and you wouldn't be wrong. This matrix broadens the concept to allow you to
apply it to any codebase you want.
## x: Abstract / Concrete
Once you have a decided which layer your code resides in, we can decide if this is concrete or abstract code.
- Concrete code is the bulk of your program. It has a single purpose that no other code performs.
- Abstract code is reusable code that does something different depending on how you invoke it.
function getUserById(id: string) {}
function groupBy<T>(list: T[], predicate: (t: T) => string) {}
## Examples
Layer | Type | File | |
---|
Transport | Concrete | <SignUpPage/> | You might have an app where there is only one page to sign up |
Transport | Abstract | <PostLayout /> | A blog will have many posts, so it would be good to have an abstract layout that can wrap every page. |
Logic | Concrete | <PasswordForm /> | In another app you might be able to sign up in a few places and so we would want to reuse the it. |
Logic | Abstract | <Button/> | A button is the ultimate reusable piece of logic. In almost every situation it acts like a button but performs a different action |
Service | Concrete | usePostData() | It's interacting with an api service to fetch some data, but it will only ever fetch Posts. |
Service | Abstract | useLocalStorage() | It's a hook to interact with the localStorage service, but it abstract and makes no decisions on what gets stored. |
## Example Client Folder Structure
I like this methodology because its clear foundation lets you customise your folder name and structure in a way that makes sense to your current project.
You can reuse a frameworks idioms or pick names that make sense to your team. Below I've mapped the concepts to a small React client.
Layer | Type | Folder |
---|
Transport | Concrete | Pages |
Transport | Abstract | Layouts |
Logic | Concrete | Features |
Logic | Abstract | Affordances |
Service | Concrete | Services |
Service | Abstract | Utils |
The bulk of code in a react app sits in the Logic layer so features
and affordances
are their own root folders.
However abstract transport and service modules are less common and feel more at home grouped inside pages
and services
.
src/
affordances/
Button.tsx
Input.tsx
features/
SignUpForm.tsx
UserProfile.tsx
pages/
layouts/
BlogPost.tsx
NormalPage.tsx
PostPage.tsx
SignUpPage.tsx
UserProfilePage.tsx
services/
util/
useLocalStorage.ts
useApi.ts
usePostItem.ts
usePostList.ts
useUserData.ts