RC
AUDIO READY — click anywhere to start
0/26
SECTION_26

Multi-Canister Communication

How canisters talk to each other on the Internet Computer — conceptual inter-canister calls, even though Caffeine runs on a single canister.

[Forward-Looking Conceptual Knowledge]

Caffeine itself runs on a single canister. This section covers conceptual Internet Computer knowledge — for when your project one day outgrows a single canister. You don't need it today, but understanding it makes your architecture decisions smarter.

> Inter-Canister Calls — Async Actor Messages

Just as every public function inside a canister is an async message, calls between canisters are async messages too. One canister sends a message to another, awaits the reply, then continues. No shared memory, no direct function calls — only message passing.

inter-canister call — async message
// Canister A — calling another canister asynchronously
import CanisterB "canister:bkyz2-fyaaa-aaaaa-qaaaq-cai";

actor CanisterA {

  // Inter-canister call — an async message to Canister B
  public shared func pingB() : async Text {
    let reply = await CanisterB.hello("from A");
    return reply;
  };

};

Async

The caller doesn't block. It sends the message and awaits — meanwhile it can handle other work.

Addressed

Every canister has a unique canister ID. Calls are routed to the target canister by that ID.

No shared state

Each canister keeps its own state. Communication happens only through messages, never shared memory.

> Query vs Update Calls Between Canisters

The same rules apply across canister boundaries. Query calls are read-only, fast, and free — but they can only be triggered directly by users, not by another canister. Update calls can write state, cost cycles, and can be triggered by other canisters.

QUERY

Read-only. Free and fast. Only callable directly by users — a canister cannot call another canister's query.

UPDATE

Writable. Costs cycles. Callable by other canisters — the primary channel for inter-canister communication.

[Why This Matters]

Because a canister can't call another canister's query, cross-canister reads usually require an update call — which costs cycles. Design to minimize frequent cross-canister reads, or consider keeping related data in the same canister.

> Principals and Caller Identification

Every canister has a principal — a unique identity. When canister A calls canister B, B can see the caller's principal. This lets you verify who is calling and only accept requests from trusted principals.

caller check — trusted principals only
// Only accept calls from a trusted principal
import Principal "mo:base/Principal";

actor Ledger {

  let owner : Principal = Principal.fromText("aaaaa-aa");

  public shared ({ caller }) func transfer() : async Bool {
    // Reject anyone who is not the trusted owner
    if (caller != owner) {
      return false;
    };
    // ... perform the transfer
    return true;
  };

};

[Security First]

Never assume a message is legitimate just because it arrived. Always check the caller principal and only allow trusted principals to call sensitive operations. This is the foundation of inter-canister security.

> Cycles — The Cost of Inter-Canister Calls

Every inter-canister call costs cycles — the network's compute and storage cost. The more calls and the more data, the higher the cost. Understanding this helps you design efficient architectures and avoid unnecessary cross-canister round trips.

01

Every call costs

Send the message, process it, return the reply — each step consumes cycles.

02

Data volume affects cost

The more data transferred, the higher the cost. Keep cross-canister messages lean.

03

Batch and cache

Combine related operations and cache frequent reads to reduce cross-canister round trips.

> Best Practices

Loose coupling, clear interfaces

Treat each canister like a service with a stable public API. Change internals freely, but keep the message contract stable.

Error handling and retries

Inter-canister calls can fail or time out. Handle errors explicitly and design idempotent operations so retries are safe.

Trusted principals only

Every call carries a caller principal. Verify it before acting — never assume a message is legitimate just because it arrived.

Versioning and upgrades

Canisters upgrade independently. Version your interfaces and plan migrations so a change in one canister never breaks another.

> When to Split vs Stay on One Canister

Caffeine runs on a single canister — the right choice for the vast majority of apps. Splitting is a forward-looking decision you consider only once your project outgrows a single canister.

01

Stay on one canister

Caffeine runs on a single canister. For most apps this is the right call — one actor, one state, no cross-canister latency, simpler upgrades.

02

Split when you outgrow it

When state, compute, or team boundaries exceed one canister, splitting becomes a forward-looking architectural decision — not a default.

03

Design for interfaces

Whether you split now or later, keep canisters loosely coupled with clear interfaces so the boundary stays cheap to change.

[Caffeine's Position]

Caffeine builds and deploys on a single canister. This section is conceptual knowledge — these principles will guide your architecture if your project one day needs multiple canisters. Today, focus on building great single-canister apps.

> The Three Things to Remember

  1. Inter-canister calls are async actor messages — no shared memory, only message passing.
  2. Always verify the caller principal — only accept calls from trusted principals.
  3. Caffeine runs on a single canister — this section is forward-looking conceptual knowledge.
Next Section