Application Architecture
An application on the Internet Computer typically consists of one or more canisters that handle backend logic, store data, and optionally serve a web frontend: all without external servers, databases, or CDNs. This page explains how these pieces fit together and what architectural patterns are available as your application grows.
The default two-canister model
Section titled “The default two-canister model”Most ICP applications start with two canisters:
- Backend canister: contains your application logic and data. You write it in Motoko or Rust (the official CDKs). Community-supported languages like TypeScript and Python are also available: see Languages. Your code is compiled locally to WebAssembly and executed by the network.
- Frontend (asset) canister: serves your web UI. It is a standard canister that hosts static files (HTML, CSS, JavaScript, images) and delivers them over HTTP.
When a user opens your application in a browser:
- The browser sends an HTTPS request to a boundary node.
- The boundary node routes the request to the frontend canister, which returns the HTML and JavaScript.
- The JavaScript uses an agent library (like
@icp-sdk/core/agent) to send messages to the backend canister. - The backend canister processes the message, updates its state if needed, and returns a response.
- The frontend renders the result.
This flow replaces the traditional web stack. There is no separate web server, application server, or database. The backend canister handles all three roles, and the frontend canister replaces your CDN.
How ICP compares to traditional architectures
Section titled “How ICP compares to traditional architectures”| Concern | Traditional web app | ICP application |
|---|---|---|
| Compute | Application server (Node, Django, etc.) | Backend canister (Wasm) |
| Storage | Database (Postgres, MongoDB, etc.) | Canister stable memory (up to 500 GiB) |
| Frontend hosting | CDN + static file server | Asset canister |
| Authentication | OAuth provider or custom auth | Internet Identity (passkey-based) |
| Scheduled tasks | Cron jobs, worker queues | Canister timers |
| External API calls | Server-side HTTP requests | HTTPS outcalls |
| Infrastructure management | You manage servers, scaling, uptime | The network handles replication and availability |
The key difference: ICP applications are self-contained. You deploy code and data to canisters, and the network provides compute, storage, and serving. There is no infrastructure to provision or maintain.
Architectural patterns
Section titled “Architectural patterns”As your application grows, you can choose from several patterns. Start simple and evolve as needed: over-architecting from the start is a common mistake.
Single canister
Section titled “Single canister”Everything (assets, logic, and data) lives in one canister. This is the simplest architecture and works well for applications serving up to thousands of users.
When to use: recommended for most applications. A single canister provides atomic operations and minimal maintenance overhead (no cycle management across canisters, no inter-canister call complexity). Consider multi-canister only when you need separation of concerns or hit a single canister’s platform limits.
Canister-per-service
Section titled “Canister-per-service”Separate canisters handle distinct responsibilities. The two-canister setup (frontend + backend) is the simplest form. You can add more canisters as responsibilities grow: one for user data, one for content, one for payments.
When to use: when you need separation of concerns between components or hit a single canister’s platform limits (memory, compute, or storage).
Things to know:
- Inter-canister calls are asynchronous. Code before and after an
awaitexecutes in separate message rounds: this affects atomicity. - Request and response payloads are limited to 2 MiB per call.
- Cross-subnet calls add one consensus round of latency compared to same-subnet calls.
For implementation details and common pitfalls, see Inter-canister calls.
Canister-per-subnet
Section titled “Canister-per-subnet”For maximum throughput, distribute canisters across multiple subnets. Each subnet processes messages independently, so spreading load across subnets lets your application scale horizontally.
When to use: high-throughput applications that exceed what a single subnet can handle (thousands of concurrent users, heavy computation).
Trade-offs: cross-subnet calls have higher latency and bandwidth limits. You need to design data partitioning carefully.
Data storage
Section titled “Data storage”Canisters store data in heap memory during execution and can persist data across upgrades using stable memory: there is no external database. Libraries provide familiar data-structure abstractions on top of raw stable memory:
- Motoko: the
corestandard library includes persistent data structures designed for upgrade-safe storage. - Rust:
ic-stable-structuresprovidesStableBTreeMapand other structures for stable memory.
For small to medium datasets, stable memory is straightforward. For applications with large data volumes (hundreds of GiB), see the canister-per-service or canister-per-subnet patterns to distribute storage across canisters.
Frontend options
Section titled “Frontend options”Not every ICP application needs the default asset canister. Your options:
- Asset canister: the standard approach. Deploy your built frontend (React, Svelte, vanilla JS, etc.) to an asset canister that serves it over HTTP. See Asset canister.
- Framework-specific canister: use a framework like Juno that provides a more opinionated hosting solution on ICP.
- Offchain frontend: host your frontend on traditional infrastructure (Vercel, Netlify, etc.) and call ICP canisters from JavaScript using
@icp-sdk/core/agent. Useful during migration or when you need features that asset canisters don’t support. - No frontend: backend-only canisters that expose a Candid API for other canisters or CLI tools to call.
Choosing an architecture
Section titled “Choosing an architecture”| Question | If yes | If no |
|---|---|---|
| Start here | Single canister: recommended for most applications | - |
| Does the app have a web UI? | Add an asset canister | Backend-only canister |
| Do you need separation of concerns or hit platform limits? | Canister-per-service | Stay with a single canister |
| Do you need to scale beyond one subnet? | Canister-per-subnet | Stay on one subnet |
Start with the simplest architecture that meets your requirements. You can always split a canister into multiple canisters later: it is much harder to merge canisters that were split prematurely.
Next steps
Section titled “Next steps”- Quickstart: deploy your first application
- Inter-canister calls: inter-canister communication patterns
- Asset canister: frontend deployment
- Canisters: canister internals