5

All the examples across the internet try to pretend that every application is run on a single thread. There is no problem with synchronization, multithreading etc. Uncle Bob, in his "Clean Architecture" book mentions the topic but very briefly and suggests to postpone some decision as long as possible.

But real applications do not look like that. Even a very simple application (like "BuckPal" from "Get your hands dirty on Clean Architecture) should take it into account.

Let's say we have a bank Account entity. In a use case we get an Account object, withdraw some money and then save the account. And we are happy. No comment about synchronization, locking etc. In typical CRUD and DB framework centric application we usually use database transaction to synchronize access.

I don't see where all the stuff related to multithreading and synchronization should be put (there is a chance those are two different topics but I don't know).

Glorfindel
  • 3,167

2 Answers2

6

Generally the best place to put all the multithreading stuff is "as far away from you as possible".

That's a joke, but also a very serious point - it's hard to get right, so most architects will try to avoid intricate or fine-grained multithreading. Web applications, as you say, tend to handle one request with one thread and let the underlying database handle data concurrency. For parallelizing work in that context it's often more reliable to outsource it to other services (microservices), which share no memory.

Hence also the popularity of immutable data structures, which can be safely shared among multiple threads without conflicting modifications.

pjc50
  • 15,223
6

First, Hexagonal/Clean Architecture is not over-engineering, it is just bad engineering for most, if not all contexts. Here is an article of mine explaining in detail what requirements Clean Architecture would imply and why that's rarely applicable to real projects, and not applicable at all to object-oriented ones.

So what about multithreading? In practice there are two things that you have to do:

  1. Make everything non-blocking. This is the hard part. In Java that's achieved with CompletableFuture, in Kotlin it's suspend functions, every language has some construct for this. It takes time to understand these things, take this time, it is very important. Unfortunately this is not transparent (in most languages) and may influence API designs.
  2. Once things are non-blocking, multithreading can be done at any point where you feel you need it, since all the constructs above already support it.

So for example to close an account you would have:

public interface Account {
   void close();
}

That is blocking, since it waits for the I/O with backend and/or database. So instead of that you have to do:

public interface Account {
   CompletableFuture<Void> close();
}

That is non-blocking (if correctly implemented), you can choose to wait on the future, but you don't have to. In any case, it will not consume a thread for the duration of the wait.

So if you want to close a 100 account simultaneously, you can write (pseudocode):

allOf(                          // Bundles all following futures
   accounts.map(Account::close) // Calls close on all accounts
).thenAccept(nothing ->         // Called when everything is complete
   log("closed all accounts");
);

This will not use 100 threads to do this, but will use the thread-pool of the backend connection "automatically".