All writing
System designSoftware architecture

Good boundaries before more services

A small system with clear responsibilities is a useful place to start.

اقرأ بالعربية
On this page

Splitting an application into services creates new places to put code. It does not automatically make the responsibilities clearer.

Before choosing how many things to deploy, ask which part of the system owns each decision.

Give each rule a home

Consider a booking application. A booking can be cancelled until a deadline. That rule belongs somewhere specific, where every path through the application can use it.

If the web interface and the background worker each implement their own version, the system has two answers to the same question.

A user sends a request to the application. Business rules live inside the application boundary, which accesses a separate database.
A boundary describes responsibility. It does not have to be a network boundary.

Make the boundary explicit

One function can express the question the rest of the application needs to ask:

type Booking = {
  status: 'confirmed' | 'cancelled';
  cancellationDeadline: Date;
};

function canCancel(booking: Booking, now: Date): boolean {
  return booking.status === 'confirmed'
    && now < booking.cancellationDeadline;
}

Passing the current time as an argument makes the rule easier to examine. This function only checks a rule; changing the booking still needs to handle concurrent requests and persistence.

Separate when you have a reason

A module can become a separately deployed service when there is a concrete need: different scaling, independent ownership, or an operational constraint.

Until then, a clear boundary inside one application is still useful. Write down what it owns and what it promises to the rest of the system.

An example essay for this new notebook. Replace it with your own writing before launch.