Intro
As you will learn working with us, we hold some software engineering principles close to our hearts especially the separation of concerns.
Respecting these principles allow us to be relatively fast and efficient while building and shipping features but this comes at a price: some constraints are to be respected.
The goal of this document is to explain how we work and the choices we made.
We hope that once you get familiar with the codebase and acquire these reflexes, coding will be as enjoyable as possible for you 😌
File structure
Domains
The front-end code is grouped by Domains.
A domain very loosely designates either a product feature (available to the user) or a helper functionality (a suite of utils fonctions achieving a particular role).
When you work on a particular functionality, chances are you will only edit files in a single domain. This is good for keeping technical debt in check [1].
If you find yourself editing a large number of files across multiple directories, chances are you need to refactor your code into a new domain. Creating new domains is allowed and encouraged!
Files
Every domain contains very specific files which are the following:
index.ts
: the entry point for every domainapi.ts
: communication with the serverlinkedin.ts
: communication with LinkedIn serverlogic.ts
: interaction with the pages and complex logiccomponents.ts
: React components or HTML populated templatestypes.ts
: TypeScript types (available publicly)- Sometimes
utils.ts
for bits of logic that don’t fit inlogic.ts
Outside of the domains, the files utils.ts
and utilsApi.ts
contain small utility functions and are available globally.
/app
directory, other important files are:
- inject.ts
, the starting point of every page reload
- inject.scss
injecting all the styles located in /styles
- popup.ts
, code related to the extension popup
However the large majority of the logic is located in /app
and this is the primary focus of this document.Organizing dependencies
Organizing the code by domains is good for grouping related bits together and avoids having to switch across multiple parts of the repository while coding.
However we found that another way to constraint our system and therefore making it more scalable was controlling where dependency injections happen [2].
Private scopes
Each domain enables a private scope, index.ts
is the only possible access point from the outside.
This enables us to import functions in the following fashion:
import * as contact from /Contact
And use them in the following way:
contact.get()
, contact.add()
, contact.update()
Which makes for cleaner and more readable code.
However, and this is the part when things can get a bit tedious, you have to manually export your functions in index.ts
to make them available to the rest of the application.
This won’t be done automatically and is a necessary step for each pull request you will submit.
The exact rules
- Files from one domain (including
index.ts
) should only import: - global files OR
- files from its own domain OR
index.ts
from other domains OR- types from anywhere
- Circular dependencies are not allowed (ie.
file1
→file2
→file3
→file1
)
(which basically means you can import from anything you want except “private” files from other domains, ie logic.ts
, api.ts
, etc.)
Resources
Here are some tech talks that go more in depth in the underlying concepts of the decisions we took:
- [1] Adam Tornhill - Prioritizing Technical Debt as if Time and Money Matters
- [2] Monica Lent - Building Resilient Frontend Architecture
- [3] Simon Brown - Modular Monoliths
- [4] Stefan Tilkov - "Good Enough" Architecture