2.2 Advanced canister calls
In the previous tutorial where you created your first dapp, you briefly learned about query and update canister calls. In this tutorial, you'll dive deeper into these types of canister calls but also take a look at advanced canister calls such as composite queries, certified variables, and inter-canister calls.
Let's first define the different types of canister calls and how they differ from one another:
Query calls are executed on a single node within a subnet. Query calls do not alter the state of a canister. They are executed synchronously and answered immediately once received.
Update calls are able to alter the canister's state. They are executed on all nodes of a subnet since the result must go through the subnet's consensus process. Update calls are submitted and answered asynchronously.
Composite queries are query calls that can call other queries (on the same subnet). They can only be invoked via ingress messages using
dfx
or through an agent such as a browser front-end. They cannot be invoked by other canisters.Certified variables are verifiable pieces of data that have an associated certificate that proves the data's authenticity. Certified variables are set using an update call, then read using a query call.
Atomic transactions refer to the execution of message handlers, which are done in isolation from one another.
Inter-canister calls are used to make calls between different canisters.
The ICP execution model
To understand how different types of canister calls are executed on ICP, first let's take a look at ICP's execution model and how it is structured.
At a high level, a canister is used to expose methods. A method is a piece of code specifying a task that declares a sequence of arguments and their associated result types. Methods return a response to the caller. Query calls, update calls, and other types of canister calls are used to call those methods to get a response.
A single method can consist of multiple message handlers. A message handler is a piece of code that can change the canister's state by taking a message, such as a request or a response, and in return produce either a response or another request. In Motoko, message handlers are separated in code by the await
keyword, which indicates that one message handler is to be executed at one time. That's because message handlers are executed atomically, or in isolation from one another.
No two message handlers within the same canister can be running at the same time. When a message handler starts executing, it receives exclusive access to the canister's memory until it finishes its execution. While no two message handlers can execute at the same time, two methods can execute at the same time.
Want to take a deeper dive? Check out an in-depth look at the ICP execution model.
Query calls
Query calls are used to query the current state of a canister or make a call to a method that operates on the canister's state. Query calls do not make any changes to the canister's state, making them 'read-only' operations. Query calls can be made to any node that hosting the canister, since the result does not go through consensus. When a query call is submitted, it is executed synchronously and answered as soon as it is received by the node. Query calls can also be used to retrieve data that is stored in a canister's stable memory.
Setting functions as query functions where appropriate can be an effective way to improve application performance, as query calls are returned faster than update calls. However, compared to update calls, the trade-off of a query call's increased performance is decreased security, since their response is not verified through consensus.
The amount of security your dapp needs depends on your dapp's use case and functionality. For example, a blog dapp that uses a function to retrieve articles matching a tag doesn't need to have requests go through consensus. In contrast, a dapp that retrieves sensitive information, such as financial data, would greatly benefit from increased security and validation that the call's response is accurate and secure. To return query results that are validated and secure, ICP supports certified variables, which you'll dive deeper into in a future module, 3.3: Certified variables.
Example query
call
Queries are defined by the function modifier query
in Motoko. Below is a simple query call used with a query the current time from the network:
import Time "mo:base/Time";
actor QueryCall {
public query func load() : async Int {
return Time.now();
};
};
View this example on ICP Ninja.
Update calls
Update calls are used to alter the state of the canister. Update calls are submitted to all nodes on a subnet and answered asynchronously. This is because update calls must go through consensus on the subnet to return the result of the call.
In comparison to query calls, update calls have the opposite trade-off of performance and security: they have a higher security level since two-thirds of the replicas in a subnet must agree on the result, but returning the result takes longer since the consensus process must be completed first.
Example update call
Update calls are not defined with a function modifier in Motoko like query calls are. Below is a simple update call that counts the number of characters within the inputted string, updates the value of the variable size
, then returns a true
or false
value if that number of characters is divisible by 2.
import Text "mo:base/Text";
import Bool "mo:base/Bool";
actor countCharacters {
public func test(text : Text) : async Bool {
let size = Text.size(text);
return size % 2 == 0;
};
};
View this example on ICP Ninja.
Certified variables
Certified variables enable queries to return an authenticated response that can be verified and trusted. They utilize chain-key cryptography to generate digital signatures that can then be validated using a single, permanent public key that belongs to ICP. The private key never exists in a single location; it is constantly distributed between many different nodes. Valid signatures can only be generated when the majority of the nodes storing the pieces of the private key participate in the signature generation process. This means an application can immediately validate all data stored in the certified variable without having to put trust into a particular node.
Certification happens at the canister level. A certified variable in a canister can be set using an update call. After the certified variable is set, its value and certification can then be read using query calls.
You'll dive deeper into certified variables in a future module, 3.3: Certified variables.
Composite queries
A composite query call is a type of query that can only be invoked via an ingress message, such as one generated by an agent in a web browser or through dfx
. Composite queries allow a canister to call a query method of another canister using an ingress query.
For example, imagine a project has one index canister and several storage canisters, with each storage canister representing a partition of the key-value stores. If a call is made that asks for a value from one of the storage canisters, composite queries allow the index canister to fetch the information from the correct canister. Without composite queries, the client would need to first query the index canister to get information about which storage canister they should call, then make a call to that canister directly themselves.
If you want to dive a bit deeper, you can read more on the developer blog or you can take a look at the DFINITY example project using composite queries.
Inter-canister calls
Inter-canister calls are used to make calls between different canisters. This is crucial for developers building complex dapps, as it enables you to use third-party canisters in your project or reuse functionality for several different services.
For example, consider a scenario where you want to create a social media dapp that includes the ability to organize events and make posts. The dapp might include social profiles for each user. When creating this dapp, you may create a single canister for storing the social profiles, then another canister that addresses event organization, and a third canister that handles social posts. By isolating the social profiles into one canister, you can create endless canisters that make calls to the social profile canister, allowing your dapp to continue to scale.
Using inter-canister calls
In this example, you have two canisters: a publisher and a subscriber. The 'Subscriber' canister sends an inter-canister call to the 'Publisher' canister that indicates it would like to subscribe to a 'topic,' which is a key-value pair. Then, the 'Publisher' canister can publish information to that topic, notifying the subscriber of the published information.
- Prerequisites
This example is currently not available in ICP Ninja and must be run locally with dfx
.
To get started, you'll be cloning the DFINITY example repo, which contains this tutorial's example plus several others. You'll be using the motoko/pub-sub
folder. Open a terminal window, navigate into your working directory (developer_liftoff
), then use the commands:
dfx start --clean --background
git clone
cd examples/motoko/pub-sub
Let's take a look at the project's files:
├── Makefile
├── README.md
├── dfx.json
└── src
├── pub
│ └── Main.mo // The smart contract code for our 'Publisher' canister.
└── sub
└── Main.mo // The smart contract code for our 'Subscriber' canister.
Writing a publisher
canister
You'll notice that this project structure is a bit different from the ones you've used thus far in this series. Since this project uses two backend canisters for its functionality, there aren't src
folders for a backend or frontend canister, just two folders for each backend canister.
Now let's take a closer look at the Main.mo
file for our 'Publisher' canister. The code can be found below with comments that explain the code's logic:
import List "mo:base/List";
// Define an actor called Publisher
actor Publisher {
// Create a new type called 'Counter' that stores a key-value pair of 'topic: value'.
type Counter = {
topic : Text;
value : Nat;
};
// Create a new type called 'Subscriber' that stores a key-value pair of 'topic: callback'. Callback refers to the inter-canister call that sends the 'Counter' key-value pair to canisters in the 'subscribers' variable.
type Subscriber = {
topic : Text;
callback : shared Counter -> ();
};
// Define a stable variable that stores the list of canisters 'subscribed' to a topic on the 'Publisher' canister.
stable var subscribers = List.nil<Subscriber>();
// Define a function that enables canisters to subscribe to a topic.
public func subscribe(subscriber : Subscriber) {
subscribers := List.push(subscriber, subscribers);
};
// Define the function to create new topics submitted by the 'Subscriber' canister within the 'Counter' key-value pair.
public func publish(counter : Counter) {
for (subscriber in List.toArray(subscribers).vals()) {
if (subscriber.topic == counter.topic) {
subscriber.callback(counter);
};
};
};
}
Writing a subscriber
canister
Now, let's look at the corresponding 'Subscriber' canister.
// Import the Publisher canister
import Publisher "canister:pub";
// Define an actor called 'Subscriber'
actor Subscriber {
// Create a new type called 'Counter' that stores a key-value pair of 'topic: value'.
type Counter = {
topic : Text;
value : Nat;
};
// Create a variable called 'count' that has a value of '0'.
var count: Nat = 0;
// Define a function that sends a call to the 'Publisher' canister that subscribes to a topic and triggers the 'updateCount' function.
public func init(topic0 : Text) {
Publisher.subscribe({
topic = topic0;
callback = updateCount;
});
};
// Define the 'updateCount' function that updates the value associated with a topic.
public func updateCount(counter : Counter) {
count += counter.value;
};
// Define a function that queries the value of 'count'.
public query func getCount() : async Nat {
count;
};
}
Deploying the project
Let's use the canisters and demonstrate their functionality. First, deploy the canisters with the command:
dfx deploy
Then, call the Subscriber canister to create and subscribe to the topic 'Astronauts.' You can do that with the command:
dfx canister call sub init '("Astronauts")'
Now, you can publish a value to be associated with the 'Astronauts' topic. Remember that the 'value' data type is defined as type Nat
, so this value must be a positive number, as Nat
does not support negative numbers.
dfx canister call pub publish '(record { "topic" = "Astronauts"; "value" = 5 })'
To verify that the data for 'Astronauts' has been created and updated correctly, let's call the Subscriber canister and ask it to retrieve the 'value' from the Publisher canister:
dfx canister call sub getCount
The output should resemble the following:
(5 : nat)

Did you get stuck somewhere in this tutorial, or do you feel like you need additional help understanding some of the concepts? The ICP community has several resources available for developers, like working groups and bootcamps, along with our Discord community, forum, and events such as hackathons. Here are a few to check out:
- Developer Discord
- Developer Liftoff forum discussion
- Developer tooling working group
- Motoko Bootcamp - The DAO Adventure
- Motoko Bootcamp - Discord community
- Motoko developer working group
- Upcoming events and conferences
- Upcoming hackathons
- Weekly developer office hours to ask questions, get clarification, and chat with other developers live via voice chat.
- Submit your feedback to the ICP Developer feedback board