Akash IKeeper vs Keeper: Understanding Module Interfaces in the Akash Network Node

The IKeeper interface declares the public contract for cross-module communication and testing, while the concrete Keeper struct implements stateful logic and store manipulation, with NewKeeper returning the interface type to enforce loose coupling.

The akash-network/node repository organizes each Cosmos SDK module using a strict separation between interface and implementation. Understanding the distinction between IKeeper and Keeper is essential for contributing to the codebase or integrating with Akash's decentralized cloud infrastructure, as this pattern appears consistently across modules like take, market, provider, and escrow.

The IKeeper Interface: Public Contracts

In x/take/keeper/keeper.go, the IKeeper interface defines the module's public API without implementation details:

type IKeeper interface {
    StoreKey() storetypes.StoreKey
    Codec() codec.BinaryCodec
    GetParams(ctx sdk.Context) (params types.Params)
    SetParams(ctx sdk.Context, params types.Params) error
    SubtractFees(ctx sdk.Context, amt sdk.Coin) (sdk.Coin, sdk.Coin, error)

    NewQuerier() Querier
    GetAuthority() string
}

This contract lists only method signatures, declaring what the module can do while hiding how it does it. Other modules import only this interface, preventing direct coupling to storage internals and allowing the implementation to evolve independently.

The Keeper Struct: Stateful Implementation

Immediately following the interface in the same file, the concrete Keeper struct holds the actual state and dependencies:

type Keeper struct {
    skey      storetypes.StoreKey
    cdc       codec.BinaryCodec
    authority string
}

The NewKeeper factory function constructs this struct but returns the interface type, encapsulating the concrete details:

func NewKeeper(cdc codec.BinaryCodec, skey storetypes.StoreKey, authority string) IKeeper {
    return Keeper{
        skey:      skey,
        cdc:       cdc,
        authority: authority,
    }
}

Unlike the interface, the concrete Keeper contains unexported fields and implements full method bodies that manipulate the Cosmos SDK store, emit events, and perform business logic like fee calculation via internal helpers such as findRate.

Key Differences Between IKeeper and Keeper

Aspect IKeeper Keeper
Purpose Declares the public contract for cross-module interaction Holds state (store keys, codec, authority) and implements logic
Definition Method signatures only; no bodies Full struct with fields and method implementations
Return Type The type returned by NewKeeper The type constructed inside NewKeeper
Visibility Exported for other modules, tests, and mocks Unexported fields; only interface methods are external
Dependencies None; pure contract References to other keepers (e.g., EscrowKeeper in market module)

The interface captures what a module provides, while the concrete keeper determines how it manipulates the blockchain state.

Dependency Injection and Cross-Module Usage

Modules interact through interface types rather than concrete imports, enabling loose coupling. For example, if a function requires access to the take module's fee logic, it accepts the interface:

func ProcessOrder(ctx sdk.Context, takeKeeper takekeeper.IKeeper) {
    params := takeKeeper.GetParams(ctx)
    fees, remainder, err := takeKeeper.SubtractFees(ctx, payment)
    // Business logic proceeds without importing concrete keeper
}

Because the parameter is takekeeper.IKeeper, the calling module does not depend on the take module's internal storage layout or initialization logic.

Testing with Mock Keepers

The interface pattern enables unit testing without a full blockchain state. Test files in testutil/cosmos/mocks/ implement IKeeper with deterministic behavior:

type MockTakeKeeper struct {
    keeper.Mock
    params types.Params
}

func (m *MockTakeKeeper) GetParams(sdk.Context) types.Params { 
    return m.params 
}

func (m *MockTakeKeeper) SubtractFees(_ sdk.Context, amt sdk.Coin) (sdk.Coin, sdk.Coin, error) {
    return amt, sdk.NewCoin(amt.Denom, sdk.ZeroInt()), nil
}

This mock satisfies takekeeper.IKeeper, allowing tests to verify cross-module logic without initializing KV stores or codecs.

Module-Specific Interface Variations

While the pattern is consistent, each module's IKeeper reflects its specific responsibilities:

  • Take: Focuses on fee management with SubtractFees and parameter getters/setters
  • Provider: Provides CRUD operations like Create, Update, and WithProviders for provider records
  • Market: Exposes complex lifecycle methods including CreateOrder, CreateBid, and OnOrderMatched, often holding references to EscrowKeeper for payment handling
  • Deployment/Escrow/Cert/Audit: Standard CRUD patterns with module-specific state management through prefix stores

Each implementation resides in its respective x/<module>/keeper/keeper.go file, following the same structural conventions established in the take module.

Summary

  • IKeeper defines the public contract for Akash modules, enabling loose coupling and interface-based mocking
  • Keeper implements stateful logic with access to store keys, codecs, and authority addresses required for Cosmos SDK operations
  • NewKeeper returns the interface type, hiding concrete implementation details and preventing direct field access
  • Cross-module communication relies exclusively on interface types, preventing circular dependencies and storage coupling
  • Unit tests leverage the interface to inject mocks rather than initializing full keeper dependencies

Frequently Asked Questions

Why does Akash use IKeeper interfaces instead of concrete Keeper structs?

The interface pattern decouples modules, allowing developers to change internal storage mechanisms in x/take/keeper/keeper.go without affecting dependent modules. It also enables test mocking, as unit tests can implement IKeeper with deterministic behavior rather than spinning up a full Cosmos SDK application with database dependencies.

Where is the IKeeper interface defined in the Akash codebase?

Each module defines its IKeeper interface at the top of the keeper/keeper.go file, immediately followed by the concrete Keeper struct. For example, see x/take/keeper/keeper.go lines 12-21 for the interface definition and lines 23-39 for the struct and factory function.

How does NewKeeper hide the concrete implementation?

The NewKeeper factory function declares a return type of IKeeper (the interface) but constructs and returns a Keeper struct internally. This means callers receive an interface value and cannot access the unexported fields (skey, cdc, authority) of the concrete struct, enforcing encapsulation and preventing direct store manipulation outside the defined methods.

Can the IKeeper interface vary between different Akash modules?

Yes, while all modules follow the same naming convention (IKeeper for interface, Keeper for struct), the method set varies based on module responsibility. The take module exposes fee-related methods like SubtractFees, while the market module includes lifecycle methods like CreateOrder and OnOrderMatched, reflecting each module's specific domain logic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →