Prowid Photos is growing: instead of two developers there are already six, and thirty are planned. On a call someone says: «Let's move to microservices — Netflix and Amazon do it». It sounds convincing, but the honest answer is «it depends». In this lesson we will look at three ways to assemble an application — a monolith, a modular monolith and microservices — what each gives and costs, how to migrate without stopping, and what to choose for Prowid Photos.

Three ways to assemble one application: a monolith, a modular monolith and microservices
Three ways to assemble one application: a monolith, a modular monolith and microservices

Monolith: the whole application in one piece

A monolith is one application that is deployed as a whole: one process, one repository, usually one database. The Prowid Photos API is exactly that now: one Express application with the sections users, photos, feed, follows and notifications.

For a start it is an excellent choice:

  • Easy to run and deploy: one command, one Docker image.
  • A call is a function call: it takes nanoseconds (the latency ladder from the first lesson) and cannot get «lost in the network».
  • One database — one transaction: uploading a photo and increasing the counter either happen together or do not happen at all.
  • Easy to debug and test: one call stack, one log, cheap.
js
// the monolith: both tables live in one database, so one transaction is enough
const client = await pool.connect();
try {
  await client.query('BEGIN');
  await client.query(
    'INSERT INTO photos (id, user_id, url) VALUES ($1, $2, $3)',
    [photoId, userId, url],
  );
  await client.query(
    'UPDATE users SET photos_count = photos_count + 1 WHERE id = $1',
    [userId],
  );
  await client.query('COMMIT'); // both changes happen, or neither
} catch (err) {
  await client.query('ROLLBACK');
  throw err;
} finally {
  client.release();
}

Problems come with growth. The code turns into a «big ball of mud»: everything imports everything, and a fix in notifications breaks the feed. Any small change requires deploying and retesting the whole application. You can scale only as a whole, even if just one part is loaded. Teams step on each other's toes in one repository.

Modular monolith: walls inside the house

Imagine a house. In a monolith there is one huge room where everyone lives together. In a modular monolith there are the same walls, but also rooms with doors inside. The application is still one process and one deploy, but the code is divided into modules by business areas with strict rules:

  • a module has a public API — one index.ts file that exports a few functions and events; everything else inside is hidden;
  • other modules import only the public API, not the files inside;
  • a module has its own tables (a schema), and it does not read other modules' tables with a JOIN;
  • modules talk with function calls or with events inside the process.
text
src/modules/
  photos/
    index.ts              <- the public API: the ONLY file others import
    photos.service.ts
    photos.repository.ts
  users/
  feed/
  notifications/