Skip to main content

Application architecture considerations

Beginner
Education
Concept

A common question when developing an application is how and where to store the data. In contrast to traditional platforms, ICP does not provide a database. Instead, ICP can persist changes in the canister state using stable memory. This means that developers have a lot of freedom in organizing and storing the data. The recommended practice is to use already existing libraries, such as the Motoko stable regions library or the Rust stable-structures library, to store data in the stable memory.

Another question that developers should ask is how to structure their application. It is possible to build an application consisting of multiple canisters that communicate with each other. A common pitfall for new developers is designing the application for millions of users from the get-go without understanding the underlying trade-offs of the system. It is better to start with the simplest possible architecture and iteratively improve it with user growth.

Canister-per-service architecture

Canisters can be thought of as microservices, where each canister is responsible for a specific service of the application, such as managing users, storing data, or processing data. Note that all benefits and disadvantages of the traditional microservice architecture apply here as well. The default project structure that dfx new generates can be viewed as the simplest microservice architecture, with the frontend canister being responsible for serving web assets and the backend canister being responsible for the core logic of the application.

Single canister architecture

A single canister can host the entire application stack, including its web assets, core logic, and data. To write a single canister that hosts frontend assets and backend core logic, you will need to use a library for the asset storage API, such as the ic-certified-assets library for Rust canisters. A few examples of single canister projects include:

Even though this architecture is simple, it can scale to thousands of users and gigabytes of data.

Canister-per-subnet architecture

ICP scales horizontally via subnets, so applications can also scale by utilizing more subnets. One way to achieve this is to have one or multiple canisters per subnet and then shard data over these canisters to distribute the load. This is the most scalable architecture and could, in theory, support millions of users and terabytes of data. Since the application data and logic are distributed over multiple subnets, this requires expert knowledge of distributed programming. The cost of development and maintenance is much higher compared to the single-canister architecture.

Canister-per-user architecture

This architecture is based on the vision that Web3 users should have full control over their data. The idea is to create a canister per user and make the user the controller of their canister. The main canister of the application would then orchestrate user canisters to implement the application’s functionality. Since users are controllers of their canisters, they can install their own code, decide how to participate in the application, and determine what data to share. These user benefits come at large development costs because the main canister needs to be programmed in such a way that it can handle all possible actions of potentially malicious user canisters. This is a new and unprecedented way of programming. There hasn’t been a successful implementation of this vision yet. A couple of projects that opted for this architecture, but only NFID Vaults have given the ownership of canisters to the users. A common misconception is that the canister-per-user architecture is the most scalable; actually, canister-per-subnet is more performant because it can utilize multiple subnets without having the overhead of too many canisters.