Rethinking Backend Architecture For A Requiem of Processes and Data
2026-3-10
"All the world is a stage"
Traditionally a web server was a single process that handled all incoming requests. the client sends a request , the server receives it, processes it, stores essential information and sends a response back to the client. all of this in a single all-or-nothing procedure.
There was a transactional euphoria to this model of thinking, we designed our database to account for this transactional model. if your server can handle N requests per second, well...that was your limit. If you wanted more... either break your application into smaller pieces or simply buy a bigger server.
Taking the problem of scaling by buying a bigger server is , this is a faustian bargain. You do get more bandwidth, but you also get more latency, If any process in your application throws, your entire application will be down. The large tower is still breakable if you move the right bricks... so you must always be wary to keep note of each brick, each module , each process of your application.
From a naive perspective, this is a nightmare to handle... but you will be surprised how well it scales, It just happens that there is no single computer that can scale infinitely... yet. But rethinking the problem, if you still need to be wary of each component of your application, why not break the application into smaller pieces? we did that too, first with polylithic architecture, then with microservices.
The theory is simple, If you break your application into manageable chunks, you can scale each chunk independently. Hell, scaling the services was no longer a problem, if anything fails , spin up a new one. That fixed the problem of scaling the business logic, but the database was still a problem.
A database has 2 roles, one, being the single source of truth, and two, being extremely hard to scale.
That was true 40 years ago, that is true today, You cannot scale a database by buying a bigger server. You have to rethink the database architecture that works well with the application architecture. Hence multiple flavors of databases, OLTP, OLAP, NoSQL.
The fringe nature of a transaction puts a lot of pressure on the database. You either have to replicate the database across multiple servers, or you have to use a sharded instance across multiple servers. regardless of the approach, the point of entry must remain same and must remain transactional. You cannot mess up your data storage architecture, you have to handle scale in a way that is consistent with the application architecture.
The problem we are trying to triumph now is no longer about scaling the application, it is about scaling the database. If your database is no the only single source of truth, You are essentially in dangerous territory. Sequential thinking is not the problem, scalability is.
If you store data in a single machine, you can handle concurrency just fine, but that does not scale. And believe me, all major database flavors have been trying best to work around this problem. RDBMS serializes concurrent transactions that can be handled by a connection pool. a bouncer or a proxy can be used on top of the connection pool to handle concurrency. Redis, mongoDB, Cassandra, all of them have their own ways of handling concurrency.
Let's take a step back , and take a look at monolith again, a single process server that had all the requirements can be broken down into 3 parts,
- Processing
- Storage
- Communication
Interestingly If we shrink this architecture , this plan of execution, we can give each data flow it's own process, it's own storage and it's own communication channels. Essentially speaking, say we have to make an e-commerce application, every time a user places an order , we sort of .. spin up a new process, a new storage and a new communication channel... an Entity .
Say, for us , an Entity is a component of the application that has 3 constituent parts, processing, storage and communication. Instead of a stateless process, an Entity holds the state and the authority over all 3 constituent parts.
A stateful long-lived child process , that is there... in-memory for each user. having the authority over that user's actions, data and communication to other entities throughout the application.
let's take an example of how users interact with an entity.
- user-123 Sends a request ( say request-1) to the application for a transaction of +100$ in their account.
- application sees the request-1 arriving from user-123 and checks the entity list for the user-123.
- if there is no entity in-memory for user, spin up a new one, collects the state from the DB, say balance = 500$ , stores it in a local variable.
- Entity sees the request-1 and decides to process it, updates the local variable . balance=600$.
- Eventually the entity syncs up the data from the local variable to the DB, for safe keeping.
- Entity lives in memory, in RAM... for future requests, just in case user decides to make another transaction.
- if no further requests are coming from user-123, the entity is destroyed, and the memory is freed up.
This is how we can isolate the application from the database scalability problem . On a large scale this is fine, these entities in essence are just javascript objects, you can spin up millions of them per second without any throughput issues.
This is a radically different approach from the separation of state we have been doing in the past. This is similar to moving from html + css + javascript to React in a way. We compose entities the same way we compose React components, handle the State and UI all in one place.
A far more approachable name for this Entity is an Actor. An actor is a stateful process that has a local state and a local authority over all 3 constituent parts of your application.
It acts on your behalf.
In real world you would have the entities being handled by a plethora of actors, each having their own authority and their own Identity.
To Identify each actor successfully, each has two thing a type + a key. Type to determine what kind of actor it is, key to identify it.
Now , Similar to how we need a framework to compose React components, We need a framework to compose actors, to manage them, some process that can create , identify and destroy them. that could look at the entity type and key to determine what address to go to find the actor.
let's take an example to hammer the idea of this framework home.
- user-123 sends a request for the bank account balance.
- the framework checks if the actor exists somewhere in it's cluster of actors.
- if yes, it gives you the address of the actor to start processing the request.
- if no, it spins up a new actor( possibly on a different machine) and gives you the address of the actor where you can send messages and communicate with it.
Logical Address Resolution
Distributed_Substrate_AWS_US_EAST_1
I want to remind you that this actor can exist in multiple machines, and the address can shift any time, so when we are talking about the Address, we are talking about the address of the actor, not the pointer to the actor... this is not a low level concept of an address. Your framework accounts for the Address and the pointers ... you just get the Address and communicate with the actor, you just need to provide the Entity type and key to start the communication channel.
Traditionally a web server was a single process that handled all incoming requests. the client sends a request , the server receives it, processes it, stores essential information and sends a response back to the client. all of this in a single all-or-nothing procedure.
There was a transactional euphoria to this model of thinking, we designed our database to account for this transactional model. if your server can handle N requests per second, well...that was your limit. If you wanted more... either break your application into smaller pieces or simply buy a bigger server.
Taking the problem of scaling by buying a bigger server is , this is a faustian bargain. You do get more bandwidth, but you also get more latency, If any process in your application throws, your entire application will be down. The large tower is still breakable if you move the right bricks... so you must always be wary to keep note of each brick, each module , each process of your application.
From a naive perspective, this is a nightmare to handle... but you will be surprised how well it scales, It just happens that there is no single computer that can scale infinitely... yet. But rethinking the problem, if you still need to be wary of each component of your application, why not break the application into smaller pieces? we did that too, first with polylithic architecture, then with microservices.
The theory is simple, If you break your application into manageable chunks, you can scale each chunk independently. Hell, scaling the services was no longer a problem, if anything fails , spin up a new one. That fixed the problem of scaling the business logic, but the database was still a problem.
A database has 2 roles, one, being the single source of truth, and two, being extremely hard to scale.
That was true 40 years ago, that is true today, You cannot scale a database by buying a bigger server. You have to rethink the database architecture that works well with the application architecture. Hence multiple flavors of databases, OLTP, OLAP, NoSQL.
The fringe nature of a transaction puts a lot of pressure on the database. You either have to replicate the database across multiple servers, or you have to use a sharded instance across multiple servers. regardless of the approach, the point of entry must remain same and must remain transactional. You cannot mess up your data storage architecture, you have to handle scale in a way that is consistent with the application architecture.
The problem we are trying to triumph now is no longer about scaling the application, it is about scaling the database. If your database is no the only single source of truth, You are essentially in dangerous territory. Sequential thinking is not the problem, scalability is.
If you store data in a single machine, you can handle concurrency just fine, but that does not scale. And believe me, all major database flavors have been trying best to work around this problem. RDBMS serializes concurrent transactions that can be handled by a connection pool. a bouncer or a proxy can be used on top of the connection pool to handle concurrency. Redis, mongoDB, Cassandra, all of them have their own ways of handling concurrency.
Let's take a step back , and take a look at monolith again, a single process server that had all the requirements can be broken down into 3 parts,
- Processing
- Storage
- Communication
Interestingly If we shrink this architecture , this plan of execution, we can give each data flow it's own process, it's own storage and it's own communication channels. Essentially speaking, say we have to make an e-commerce application, every time a user places an order , we sort of .. spin up a new process, a new storage and a new communication channel... an Entity .
Say, for us , an Entity is a component of the application that has 3 constituent parts, processing, storage and communication. Instead of a stateless process, an Entity holds the state and the authority over all 3 constituent parts.
A stateful long-lived child process , that is there... in-memory for each user. having the authority over that user's actions, data and communication to other entities throughout the application.
let's take an example of how users interact with an entity.
- user-123 Sends a request ( say request-1) to the application for a transaction of +100$ in their account.
- application sees the request-1 arriving from user-123 and checks the entity list for the user-123.
- if there is no entity in-memory for user, spin up a new one, collects the state from the DB, say balance = 500$ , stores it in a local variable.
- Entity sees the request-1 and decides to process it, updates the local variable . balance=600$.
- Eventually the entity syncs up the data from the local variable to the DB, for safe keeping.
- Entity lives in memory, in RAM... for future requests, just in case user decides to make another transaction.
- if no further requests are coming from user-123, the entity is destroyed, and the memory is freed up.
This is how we can isolate the application from the database scalability problem . On a large scale this is fine, these entities in essence are just javascript objects, you can spin up millions of them per second without any throughput issues.
This is a radically different approach from the separation of state we have been doing in the past. This is similar to moving from html + css + javascript to React in a way. We compose entities the same way we compose React components, handle the State and UI all in one place.
A far more approachable name for this Entity is an Actor. An actor is a stateful process that has a local state and a local authority over all 3 constituent parts of your application.
It acts on your behalf.
In real world you would have the entities being handled by a plethora of actors, each having their own authority and their own Identity.
To Identify each actor successfully, each has two thing a type + a key. Type to determine what kind of actor it is, key to identify it.
Now , Similar to how we need a framework to compose React components, We need a framework to compose actors, to manage them, some process that can create , identify and destroy them. that could look at the entity type and key to determine what address to go to find the actor.
let's take an example to hammer the idea of this framework home.
- user-123 sends a request for the bank account balance.
- the framework checks if the actor exists somewhere in it's cluster of actors.
- if yes, it gives you the address of the actor to start processing the request.
- if no, it spins up a new actor( possibly on a different machine) and gives you the address of the actor where you can send messages and communicate with it.
I want to remind you that this actor can exist in multiple machines, and the address can shift any time, so when we are talking about the Address, we are talking about the dynamic logical address of the actor, not the pointer to the actor... this is not a low level concept of an address. Your framework accounts for the Address and the pointers ... you just get the Address and communicate with the actor, you just need to provide the Entity type and key to start the communication channel.
To shard your application logic across multiple machines , needs the orchestration framework to be more than just a simple address lookup though. One design change that might help us in this regard is to specify a cluster.
A cluster in this regard is a group of machines that are under the same orchestration framework. Although we can use isolated machines with the framework intact, and orchestrate the load across using keubernetes, but doing it this way can allow some super powers that running on keubernetes might not. let's look at the specification of a cluster first.
A cluster :
- agrees on who handles which actor or entity types.
- forwards requests to the right node in the cluster.
- detects the runner crashing and orchestrates the recovery.
- adds new runners with new downtime.
a runner or a node in this instance specifies a machine running our framework. We will use a database instance to store the runner states , where runners can update their state similar to a heartbeat. If a runner goes down, the cluster can detect it , un-assign it's members and re assign them to other nodes, the database is the source or orchestration instead of source of truth for now.
Now , onto the actual reason why we are taking this orchestration approach. the membership and assignment of actors to nodes is handled by the cluster.
for this reason it creates shards for the application logic, meaning each shard is a virtual range of actors in the application. let's take another example to understand how this works.
- user-A 's actor is stored in shard-1.
- user-B's actor is stored in shard-1.
- user-C's actor is stored in shard-2.
to assign an actor to a shard, we have a hashing function that determines which shard the actor could be assigned to, this is a solved problem in the industry.
say user-D -> hash(user-D) -> shard-2. since it is the least loaded shard, we assign it to user-D.
this shard key is returned back to the cluster so next time the user sends a request, we send the Entity type + key + Shard-key to the cluster and their session is assigned to the shard.
the cluster handles the logical assignment of these shards across the runners. each runner can host one shard or multiple shards, but the cluster is responsible for lookup.
let's look at 2 workflows to understand this better.
- When the Runner is Active.
- user-A sends a request
- hash(user-1) -> shard-1-> Runner-1 holds the shard-1.
- Runner-1 receives the request, writes to the cluster database, hands over the request to the actor.
- the actor processes the request and returns the result to the runner.
- Runner-1 receives the result, writes to the cluster database, returns the result to the user-A.
- request is marked as 'processed' in the cluster database.
- When the Runner is Down.
- user-A sends a request.
- hash(user-1) -> shard-1 -> Runner-1 held the shard-1 but it is down.
- message is written to the cluster database.
- the cluster sets a timeout of 35 ms .
- if in 35 ms the message is not processed by runner-1, the cluster assigns the shard-1 to another runner , say Runner-2.
- Runner-2 queries the cluster database for the shard-1's messages that were missed in the meantime.
- Runner-2 replays the missed messages to the actors
- Failover is alerted and progressively re-started.
This failover is why the Effect cluster requires a database, this database process needs not be as big as your application database , but a small instance ( something like sqlite ) is enough to handle the throughput, we can also use an in-memory database like Redis.
Now , Finally we can talk about the anatomy of an actor,
- It holds an Entity type+ Key + Shard-key. meaning for every user , there is a single actor holding everything together.
- An actor maintains a mailbox for incoming requests to sequentially process them, while being a concurrent model.
- From the caller's perspective sure if the actor dies , there might be a small delay , but in the long run , it is far better to have a single actor fail than to have your entire application fail, or your database choke up under constant load. better to fail one person than all the customers. The actor regardless , maintains a persisted step for critical operations , so chaining actors do not result in a single point of failure.
When an actor gets a message from another actor, three things can happen ,
- sends the message across to another actor.
- process the message and update it's state.
- return a response for the user.
A problem you could run into is that the actors might be blocking each other while processing the messages back and forth. you can follow the continuation pattern to avoid this. the trick being.
say Actor-A-> Actor-B-> Actor-C . and after processing the message , Actor-C sends a message back to Actor-A where the response is shared back to the user.
for the entire chain to work, Actor A cannot be blocking while other 2 are working , so actor A must send a callback to actor B , and when processed , the same must be sent to actor C. when both are done , the callback is updated and Actor A is called, the actor persists the callback as messages and the message state is the persisted step we talked earlier during the anatomy of an actor.
essentially, we commit the state of the actor to a durable store, so that when the actor dies midway, the state is not lost , can be picked up from the store and the actor can continue from where it left off. essentially a write ahead log for each request. The key insight is this: you are not persisting the result of an operation. You are persisting the intention to perform one. As long as the input of each step is committed before the step runs, the system can always replay it to the same output.
This is why the cluster database exists. Not to store your user's data but to store the execution trace of your actors, so that crash recovery is not a prayer but a guarantee.
there are tradeoffs though. a question you have to answer honestly: when is your actor's local state the truth, and when is the database the truth?
In the happy path, both agree. The actor holds the working copy, the database holds the committed copy. They are always converging.
But in the window between an actor update and a database sync, there is a gap. Two users querying the same account from different actors could theoretically see different balances.
If you have designed your system so that one actor owns one user's data ... and no other actor ever touches that user's data directly ...the gap is contained. You have serialized all writes for that entity through a single queue. That is your consistency guarantee.
The moment you break that rule the moment two actors share authority over the same data you have re-introduced the concurrency problem you were trying to escape.
The actor model does not prevent distributed data races. It eliminates them by design. And a bad designer always leads to a bad implementation, no matter how well the model composes.
This Actor model as we call it now is the same Actor Model, formalized by Carl Hewitt in 1973. used by the Erlang language, JVM, Cloudflare Durable objects or as I like to call them... Actors living on the edge. Temporal .... take another step back.
you can see the lineage clearly. Erlang proved the model at the language level in the 1980s. Microsoft Orleans brought it to the cloud in the 2010s. Temporal made it a managed product you can integrate without building the infrastructure. Cloudflare embedded it into their global edge network. And Vercel has now made it a directive you add to a function.
Each generation made the idea more accessible. The fundamental mechanics single-threaded entity, message mailbox, persisted execution steps, crash recovery through replay have not changed. Only the abstraction layer has risen.
The trade-off that none of them fully escape is this: durability costs a write. Every persisted step is a round-trip to storage. You are trading throughput for recoverability. For most data-intensive applications payments, orders, bookings, AI agent loops ...that is the right trade. For high-frequency event streams where you can afford to lose a message, it is overkill.
The actor model does not make distributed systems easy. It makes them bearable. It gives the hardest problems concurrency, consistency, failure ... a clear home. One entity, one authority, one queue of work. Everything else is just plumbing.
And increasingly, the plumbing is being handled for you. mario would be proud.
fin.