Skip to content

Instantly share code, notes, and snippets.

@marcusradell
Created January 31, 2023 19:53
Show Gist options
  • Save marcusradell/a6fb932e65b0ee8e7df9c600105e45d7 to your computer and use it in GitHub Desktop.
Save marcusradell/a6fb932e65b0ee8e7df9c600105e45d7 to your computer and use it in GitHub Desktop.

Janit Backend

Our modular monolith server for Janit.

Values

  • High quality.
  • Simple solutions.
  • Early feedback.
  • Keep costs low.

Principles

  • Start with shipping. We work with trunk-based development by committing straight to our main branch. We protect the new code with feature flags. We show early results to enable team discussions and further planning.

  • Don't overshoot. Delivering extra functionality slows down feedback and makes the solution less simple. This will lead to lower quality. Think of it like hitting a golf ball too hard.

  • Create the pain before you remove it. Even if you know that you will need some functionality, you need to first see what the problem looks like. This means, for example, that we create duplication before removing it.

  • Tidy up before you move on. If you don't tidy up your features, it creates an illusion of progress that piles up into a hidden tech debt. By tidying up the code continuously, we lower maintenance costs and can sustain our pace of delivery.

Infrastructure

Janit has a single backend application that runs on Google Cloud Platform's (GCP) Cloud Run.

We use GitHub Actions for continuous integration and continuous deployment.

Docker is used to contain the built code that is then pushed to to GCP's Artifact Registry.

GCP Cloud SQL for PostgreSQL host our database.

Application Tech Stack

We use node.js as we want all programmers to be able to work across our entire stack.

tRPC gives us typesafe JSON web API calls. We complement this with a few normal http endpoints for status checks and file uploads.

Zod is used for validating our input.

Prisma gives us a typesafe DB client. We only let one domain (a domain is an internal service) access its own tables. We do this by picking the Prisma models that we need using TypeScript.

Code architecture

Our app layer creates all of our domains, their dependencies, and mounts the routes. The app is run in a separate module to make it simpler to integration test. We can also test manually by setting process.env.SANDBOX=ON and then writing code in src/sandbox.ts.

We divide our code into domains. These are our internal services that solve our business problems. Each domain will have an Application Public Interface (API) that the app and other domains can interact with. The domain aims to be self-contained with unit tests, routes, types, etc. The internal layers inside the domain is usually invisible to the outside, making experimentation possible without dirtying the entire application. Each domain can be separated out into a microservice with minimal effort.

A domain's API-layer handles the business logic. Many methods in our domain will be simple transaction scripts; they validate client inputs, add some timestamps, generates UUIDs, and then stores it in our DB. These should be integration tested to provide documentation and some basic automated testing. Other parts will have domain logic in them, in which case we will unit test these parts. Each API method will be tested as an isolated unit.

The domain router is exported as a static factory function. We mount all the domain routers in the app layer. Sometimes, we need to feature flag a new domain router, or have duplicate routers for the same domain for backwards compatibility. When using the sandbox mode, we usually don't want the routers at all.

Each domain will usually have access to the database (DB). A prisma client is shared between domains, and the domains will internally restrict themselves to only use what it needs. The repository layer is internal to the domain to keep every domain as isolated as possible. This is key to be able to maintain and scale our system in the future. A side effect is that we cannot use foreign keys or transactions between tables that exist in different domains. This needs to be solved using standard microservices patterns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment