Developers Home»Docs»Coupling Pallets

Coupling Pallets

In computer science, coupling is the degree to which two software modules depend on each other. System designers use the terms high and low coupling to describe how computer systems are structured. The term also applies to object oriented programming paradigms, whereby tight coupling is when two groups of classes are dependant on each other, and loose coupling is when a class uses an interface that another class exposes.

In Substrate, tight and loose pallet coupling is used to for calling a function that lives inside another pallet. Both techniques achieve the same thing in different ways, each having certain trade-offs. In a nutshell, tight coupling of pallets should be used in instances where a pallet requires inheriting its coupled counterpart as a whole as opposed to specific types or methods. In general, tight coupling makes working with two pallets less flexible and extensible.

Tightly coupled pallets

Tightly coupling pallets is more explicit than loosely coupling them. When writing a pallet that tightly couple, you explicitly specify the pallet's Config trait to be bound by the Config trait of the other pallet you want to couple with.

Notice that all FRAME pallets are tightly coupled to the frame_system pallet. Here's an example of tightly coupling a pallet with the Config trait of an imaginary pallet called some_pallet in addition to frame_system:

pub trait Config: frame_system::Config + some_pallet::Config {
    // --snip--
}

This is very similar to using class inteheritance in object oriented programming. Supplying this trait bound implies that this pallet can only be installed in a runtime that also has some_pallet pallet installed. Similar to with frame_system, this approach would require specifying some_pallet in the coupled pallet's Cargo.toml file.

Tight coupling has several disadvantages developers should take into account:

  • Maintainability: changes in one pallet will often result in needing to modify the other pallet.
  • Reusability: both modules must be included for one to be used, making it more difficult to integrate a tightly coupled pallet.

Loosely coupled pallets

Unlike tight coupling, in loose coupling pallet we just specify traits and function interfaces that certain types need to be bound by.

The actual implementation of such types happens outside of the pallet during our runtime configuration (usually in the /runtime/src/lib.rs file). Here one may choose to configure it with another pallet that has implemented these traits, or declare a totally new struct, implement those traits, and assign it when implementing the pallet config in runtime.

Let's go through an example. Say in one pallet we want to tap into one's account balance and make a transfer to another account. We first define a Currency trait, which has an abstract function interface that is agreed will implement the actual transfer logic later.

pub trait Currency<AccountId> {
    // -- snip --
    fn transfer(
        source: &AccountId,
        dest: &AccountId,
        value: Self::Balance,
        // don't worry about the last parameter for now
        existence_requirement: ExistenceRequirement,
    ) -> DispatchResult;
}

Then inside our own pallet, we define MyCurrency associated type and bound it by Currency<Self::AccountId> so we can tap into the balance tranfer logic by calling T::MyCurrency::transfer(...).

pub trait Config: frame_system::Config {
    type MyCurrency: Currency<Self::AccountId>;
}

impl<T: Config> Pallet<T> {
    pub fn my_function() {
        T::MyCurrency::transfer(&buyer, &seller, price, ExistenceRequirement::KeepAlive)?;
    }
}

Notice that at this point, we have not specified how the Currency::transfer() logic will be implemented. It is only agreed upon that it will be implemented somewhere.

Now in our runtime configuration (i.e. runtime/src/lib.rs), we have our runtime implements the pallet, and concretely specify the type to be Balances.

impl my_pallet::Config for Runtime {
    type MyCurrency = Balances;
}

Balances, which is specified in construct_runtime! macro, is of type pallet_balances that implements Currency trait.

By now we have closed the loop and provide on implementation to Currency<AccountId> trait.

Further notes

Notice that many FRAME pallets are coupled to this Currency trait in this way.

In general, loose coupling will provide more flexibility than tight coupling and is considered better practice from a system design perspective. It guarantees better maintability, reusability, and extensibility of code. Yet, tight coupling can be a good first choice for scenarios where the pallets are minimally complex and have more overlap in methods and types than differences.

In FRAME, there exists 2 pallets that are tightly coupled to pallet_treasury: the Bounties pallet and the Tipping pallet.

As a general rule, the more complex a pallet is, the less desirable it would be to tightly couple it. This evokes a concept in computer science called cohesion, a metric used to examine the overall quality of a software system.

Learn more

Last edit: on

Run into problems?
Let us Know