← Back to list

Calling Functions Between Hyperledger Fabric Chaincodes: The Right Way

If you’ve deployed a couple of chaincodes on Hyperledger Fabric, you’ve probably faced this question: “How do I reuse a function from one…

Muhammad Talha in CoinsBench · 2026-06-09 07:53 · 3 claps · 10.0 min read
#hyperledger #private-blockchain #blockchain #chaincode #smart-contracts
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3

Calling Functions Between Hyperledger Fabric Chaincodes: The Right Way

If you’ve deployed a couple of chaincodes on Hyperledger Fabric, you’ve probably faced this question: “How do I reuse a function from one chaincode in another without duplicating code?”

The answer isn’t immediately obvious, and many developers end up with duplicated logic across chaincodes or, worse, try to share files directly between projects. Both approaches lead to maintenance nightmares and architectural problems.

In this post, I’ll walk you through the professional way to share functionality between chaincodes using cross-chaincode invocation — and why it’s the right architectural choice.

The Problem: Why Code Duplication Doesn’t Scale

Let’s say you’re building a consortium blockchain for educational verification. You have:

  1. Digital Identity Chaincode — Validates users, manages email/phone verification
  2. Degree Verification Chaincode — Issues and verifies degrees

Both need to validate email addresses and check if a user exists. You have three choices:

  1. Copy-paste the functions into both chaincodes (bad)
  2. Share files directly using relative imports like require('../common/validators.js') (worse)
  3. Use cross-chaincode invocation (correct)

Option 1 and 2 break the fundamental principle of microservices: independent deployment. If you update email validation logic, you’d need to redeploy both chaincodes. Plus, you risk version mismatches and consistency issues.

The professional approach is cross-chaincode invocation — one chaincode explicitly calls functions in another chaincode through the Fabric SDK.

Understanding Cross-Chaincode Invocation

Cross-chaincode invocation allows a chaincode to invoke functions in another chaincode on the same channel. It’s essentially an RPC (Remote Procedure Call) within the blockchain network.

How It Works (Conceptually)

┌─────────────────────────────────────────────────────────┐
│  Degree Verification Chaincode                          │
│                                                         │
│  func IssueDegree(ctx, userID, degreeData) {            │
│      // Need to validate user first                     │
│      call Identity Chaincode                            │
│      └─→ CheckUserExists(userID) ───────┐               │
│                                          │              │
│                                          ▼              │
│                           ┌──────────────────────────┐  │
│                           │ Digital Identity         │  │
│                           │ CheckUserExists(id)      │  │
│                           │ ValidateEmail(email)     │  │
│                           │ GetUser(id)              │  │
│                           └──────────────────────────┘  │
│      ← Response: true/false                             │
│      if valid, proceed with degree issuance             │
│  }                                                      │
└─────────────────────────────────────────────────────────┘

The Core Fabric API

In Go, you invoke another chaincode using the ChaincodeStubInterface:

response := ctx.GetStub().InvokeChaincode(
    "target-chaincode-name",           // Name of chaincode to call
    [][]byte{
        []byte("FunctionName"),        // Function to invoke
        []byte("arg1"),                // Arguments
        []byte("arg2"),
    },
    "channel-name",                    // Channel where chaincode exists
)
// Check response
if response.Status != 200 {
    return fmt.Errorf("failed: %s", response.Message)
}
// Process response ([]byte format)
resultData := response.Payload

This is powerful because:

  • Both chaincodes remain independently deployable
  • No code duplication
  • Proper versioning control
  • Clean separation of concerns
  • Follows distributed systems best practices

Deep Dive: Setting Up Cross-Chaincode Communication

Step 1: Create the Identity Chaincode (Shared Functions)

Here’s a minimal but complete Digital Identity chaincode with the functions we want to share:

// chaincode/identity/identity.go
package main
import (
    "encoding/json"
    "fmt"
    "regexp"
    "github.com/hyperledger/fabric-contract-api-go/contractapi"
)
type DigitalIdentityContract struct {
    contractapi.Contract
}
type User struct {
    UserID      string `json:"user_id"`
    Email       string `json:"email"`
    PhoneNumber string `json:"phone_number"`
    Status      string `json:"status"`
    CreatedAt   string `json:"created_at"`
}
// SHARED FUNCTION 1: Validate Email
func (c *DigitalIdentityContract) ValidateEmail(
    ctx contractapi.TransactionContextInterface,
    email string,
) (bool, error) {
    // RFC 5322 simplified regex for email
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

    if !emailRegex.MatchString(email) {
        return false, nil
    }

    return true, nil
}
// SHARED FUNCTION 2: Check User Exists
func (c *DigitalIdentityContract) CheckUserExists(
    ctx contractapi.TransactionContextInterface,
    userID string,
) (bool, error) {
    userBytes, err := ctx.GetStub().GetState(userID)
    if err != nil {
        return false, err
    }

    return userBytes != nil, nil
}
// SHARED FUNCTION 3: Get User Data
func (c *DigitalIdentityContract) GetUser(
    ctx contractapi.TransactionContextInterface,
    userID string,
) (*User, error) {
    userBytes, err := ctx.GetStub().GetState(userID)
    if err != nil {
        return nil, err
    }

    if userBytes == nil {
        return nil, fmt.Errorf("user not found")
    }

    var user User
    if err := json.Unmarshal(userBytes, &user); err != nil {
        return nil, err
    }

    return &user, nil
}
// Public function: Create User
func (c *DigitalIdentityContract) CreateUser(
    ctx contractapi.TransactionContextInterface,
    userID, email, phone string,
) error {
    // Validate email first
    isValid, err := c.ValidateEmail(ctx, email)
    if err != nil || !isValid {
        return fmt.Errorf("invalid email format")
    }

    user := User{
        UserID:      userID,
        Email:       email,
        PhoneNumber: phone,
        Status:      "active",
        CreatedAt:   "2024-01-15",
    }

    userJSON, _ := json.Marshal(user)
    return ctx.GetStub().PutState(userID, userJSON)
}
func (c *DigitalIdentityContract) InitLedger(
    ctx contractapi.TransactionContextInterface,
) error {
    return nil
}

Step 2: Create the Degree Chaincode (Calls Identity)

Now let’s create the Degree chaincode that calls functions from the Identity chaincode:

// chaincode/degree/degree.go
package main
import (
    "encoding/json"
    "fmt"
    "github.com/hyperledger/fabric-contract-api-go/contractapi"
)
type DegreeContract struct {
    contractapi.Contract
}
type Degree struct {
    DegreeID     string `json:"degree_id"`
    UserID       string `json:"user_id"`
    University   string `json:"university"`
    Field        string `json:"field"`
    GPA          string `json:"gpa"`
    VerifiedDate string `json:"verified_date"`
    Status       string `json:"status"` // "issued", "revoked"
}
// Helper: Safely invoke another chaincode with error handling
func (c *DegreeContract) invokeIdentityChaincode(
    ctx contractapi.TransactionContextInterface,
    function string,
    args ...string,
) ([]byte, error) {

    // Build the argument array
    invokeArgs := make([][]byte, 0)
    invokeArgs = append(invokeArgs, []byte(function))

    for _, arg := range args {
        invokeArgs = append(invokeArgs, []byte(arg))
    }

    // Invoke the identity chaincode
    response := ctx.GetStub().InvokeChaincode(
        "identity-chaincode",    // Must match the name deployed on Fabric
        invokeArgs,
        "mychannel",             // Must be the same channel
    )

    // Check for errors
    if response.Status != 200 {
        return nil, fmt.Errorf(
            "identity-chaincode invocation failed (status %d): %s",
            response.Status,
            response.Message,
        )
    }

    if response.Payload == nil {
        return nil, fmt.Errorf("empty response from identity-chaincode")
    }

    return response.Payload, nil
}
// Public function: Issue Degree (uses cross-chaincode call)
func (c *DegreeContract) IssueDegree(
    ctx contractapi.TransactionContextInterface,
    degreeID, userID, university, field, gpa string,
) error {

    // Step 1: Call identity chaincode to verify user exists
    userExistsPayload, err := c.invokeIdentityChaincode(
        ctx,
        "CheckUserExists",
        userID,
    )
    if err != nil {
        return fmt.Errorf("failed to check user: %w", err)
    }

    // Parse response (should be "true" or "false")
    userExists := string(userExistsPayload) == "true"
    if !userExists {
        return fmt.Errorf("user does not exist in identity system")
    }

    // Step 2: Create and store the degree
    degree := Degree{
        DegreeID:     degreeID,
        UserID:       userID,
        University:   university,
        Field:        field,
        GPA:          gpa,
        VerifiedDate: "2024-01-15",
        Status:       "issued",
    }

    degreeJSON, _ := json.Marshal(degree)

    if err := ctx.GetStub().PutState(degreeID, degreeJSON); err != nil {
        return fmt.Errorf("failed to store degree: %w", err)
    }

    return nil
}
// Public function: Verify Degree
func (c *DegreeContract) VerifyDegree(
    ctx contractapi.TransactionContextInterface,
    degreeID string,
) (string, error) {

    degreeBytes, err := ctx.GetStub().GetState(degreeID)
    if err != nil {
        return "", err
    }

    if degreeBytes == nil {
        return "", fmt.Errorf("degree not found")
    }

    var degree Degree
    if err := json.Unmarshal(degreeBytes, &degree); err != nil {
        return "", err
    }

    return degree.Status, nil
}
func (c *DegreeContract) InitLedger(
    ctx contractapi.TransactionContextInterface,
) error {
    return nil
}

Installation & Setup Across Operating Systems

Linux Setup

# 1. Install Go 1.20+
wget https://go.dev/dl/go1.20.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
# 2. Initialize both chaincodes
cd chaincode/identity
GO111MODULE=on go mod init github.com/yourorg/identity-chaincode
go mod tidy
cd ../degree
GO111MODULE=on go mod init github.com/yourorg/degree-chaincode
go mod tidy
# 3. Package chaincodes
cd ../../
peer lifecycle chaincode package identity.tar.gz \
  --path ./chaincode/identity \
  --lang golang \
  --label identity_1.0
peer lifecycle chaincode package degree.tar.gz \
  --path ./chaincode/degree \
  --lang golang \
  --label degree_1.0
# 4. Install on peers
peer lifecycle chaincode install identity.tar.gz
peer lifecycle chaincode install degree.tar.gz
# 5. Approve and commit (see Fabric documentation)

macOS Setup

# 1. Install Go via Homebrew
brew install go@1.20
export PATH="/usr/local/opt/go@1.20/bin:$PATH"
# 2. Verify installation
go version  # Should show go1.20.x
# 3. Follow same steps as Linux from step 2 onwards
cd chaincode/identity
GO111MODULE=on go mod init github.com/yourorg/identity-chaincode
go mod tidy
# Continue with packaging and installation...

Windows Setup

# 1. Download and install Go 1.20+
# Visit: https://go.dev/dl/
# Choose windows-amd64.msi and run installer
# 2. Verify installation (PowerShell as Admin)
go version
# 3. Set environment variables
$env:GO111MODULE = "on"
# 4. Initialize chaincodes
cd chaincode/identity
go mod init github.com/yourorg/identity-chaincode
go mod tidy
cd ../degree
go mod init github.com/yourorg/degree-chaincode
go mod tidy
# 5. For peer commands, use Fabric binaries
# Download from: https://github.com/hyperledger/fabric/releases

Comparing Approaches: Why InvokeChaincode is Superior

Before committing to this approach, let’s compare the three options:

Verdict: InvokeChaincode wins for professional, scalable systems. Yes, there’s slight latency, but the architectural benefits far outweigh this cost.

Critical: Common Mistakes That Will Cause Failures

Mistake 1: Wrong Chaincode Name

// WRONG - Name doesn't match deployment
response := ctx.GetStub().InvokeChaincode(
    "identity",  // Deployed as "identity-chaincode"!
    args,
    "mychannel",
)
// CORRECT
response := ctx.GetStub().InvokeChaincode(
    "identity-chaincode",  // Match exactly
    args,
    "mychannel",
)

Error you’ll see: 500 - chaincode not found

Mistake 2: Function Name Case Sensitivity

// WRONG - Function doesn't exist
invokeArgs := [][]byte{
    []byte("checkuserexists"),  // Wrong case!
}
// CORRECT
invokeArgs := [][]byte{
    []byte("CheckUserExists"),  // Matches Go function name
}

Error you’ll see: unknown function checkuserexists

Mistake 3: Different Channels

// WRONG - Chaincodes on different channels
// identity-chaincode is on "channel-a"
// degree-chaincode is on "channel-b"
response := ctx.GetStub().InvokeChaincode(
    "identity-chaincode",
    args,
    "channel-b",  // Can't invoke across channels!
)
// CORRECT - Deploy both on same channel
response := ctx.GetStub().InvokeChaincode(
    "identity-chaincode",
    args,
    "mychannel",  // Both exist here
)

Error you’ll see: chaincode not available on this channel

Mistake 4: Not Checking Response Status

// WRONG - Assuming success
response := ctx.GetStub().InvokeChaincode(...)
resultData := response.Payload  // Could be nil or error!
// CORRECT - Always validate
response := ctx.GetStub().InvokeChaincode(...)
if response.Status != 200 {
    return nil, fmt.Errorf("invocation failed: %s", response.Message)
}
if response.Payload == nil {
    return nil, fmt.Errorf("empty response received")
}
resultData := response.Payload

Mistake 5: Not Handling Type Conversions

// WRONG - Payload is []byte, not bool
response := ctx.GetStub().InvokeChaincode(...)
userExists := response.Payload  // This is []byte!
if userExists {  // Type error!
    // ...
}
// CORRECT - Convert to proper type
response := ctx.GetStub().InvokeChaincode(...)
userExists := string(response.Payload) == "true"
if userExists {
    // ...
}

Handling Multiple Channels: A Practical Approach

In enterprise setups, you might have:

  • Channel A: Between Organizations X and Y
  • Channel B: Between Organizations Y and Z

You can’t directly invoke across channels, but you can design around this:

// Pattern: Create a routing chaincode if you need cross-channel logic
func (c *DegreeContract) IssueDegreeAcrossChannels(
    ctx contractapi.TransactionContextInterface,
    degreeID, userID string,
) error {
    // For channel-specific validation, call the appropriate chaincode
    channelID := ctx.GetStub().GetChannelID()  // Get current channel

    var identityChaincodeOnChannel string

    // Route to correct chaincode based on channel
    switch channelID {
    case "channel-a":
        identityChaincodeOnChannel = "identity-chaincode-a"
    case "channel-b":
        identityChaincodeOnChannel = "identity-chaincode-b"
    default:
        return fmt.Errorf("unknown channel: %s", channelID)
    }

    // Invoke the appropriate chaincode
    response := ctx.GetStub().InvokeChaincode(
        identityChaincodeOnChannel,
        [][]byte{[]byte("CheckUserExists"), []byte(userID)},
        channelID,
    )

    if response.Status != 200 {
        return fmt.Errorf("validation failed: %s", response.Message)
    }

    return nil
}

Transaction Atomicity: What You Need to Know

Here’s the truth: Cross-chaincode invocations are NOT atomic. This is important.

If you invoke a chaincode like this:

func (c *DegreeContract) IssueDegree(ctx contractapi.TransactionContextInterface, userID string) error {

    // Call 1: Validate user
    response1 := ctx.GetStub().InvokeChaincode(...)
    if response1.Status != 200 {
        return fmt.Errorf("validation failed")
    }

    // Call 2: Create degree
    ctx.GetStub().PutState(...)

    // What if Call 2 fails after Call 1 succeeds?
    // Both are logged, but not as a single atomic unit
}

What this means:

  • Each cross-chaincode invocation is a separate transaction
  • If your logic fails after calling another chaincode, the other chaincode’s state already committed
  • You need to design your business logic accordingly

How to handle this:

// Use a transaction ID to track related operations
func (c *DegreeContract) IssueDegreeWithTracking(
    ctx contractapi.TransactionContextInterface,
    degreeID, userID string,
) error {
    txID := ctx.GetStub().GetTxID()  // Unique transaction ID

    // Validate first
    response := ctx.GetStub().InvokeChaincode(...)
    if response.Status != 200 {
        // Log the failed transaction with txID for audit trail
        return fmt.Errorf("validation failed for tx %s", txID)
    }

    // Only proceed if validation succeeded
    degree := Degree{
        DegreeID:      degreeID,
        UserID:        userID,
        TransactionID: txID,  // Link to validation tx
    }

    degreeJSON, _ := json.Marshal(degree)
    return ctx.GetStub().PutState(degreeID, degreeJSON)
}

This way, even though the transactions aren’t atomic, you have a complete audit trail.

Security Considerations

1. Validate All Inputs

// DON'T - Trust user input
func (c *DegreeContract) IssueDegree(ctx contractapi.TransactionContextInterface, userID string) error {
    response := ctx.GetStub().InvokeChaincode(
        "identity-chaincode",
        [][]byte{[]byte("CheckUserExists"), []byte(userID)},
        "mychannel",
    )
}
// DO - Sanitize and validate
func (c *DegreeContract) IssueDegree(ctx contractapi.TransactionContextInterface, userID string) error {
    if userID == "" {
        return fmt.Errorf("userID cannot be empty")
    }
    if len(userID) > 255 {
        return fmt.Errorf("userID too long")
    }

    response := ctx.GetStub().InvokeChaincode(...)
}

2. Check Endorsement Policies

When degree-chaincode calls identity-chaincode, both chaincodes must be endorsed. Configure your endorsement policies to require appropriate organizations:

# In your channel policy
/Channel/Application/DegreeEndorsement: 
  AND('Org1MSP.peer', 'Org2MSP.peer')
/Channel/Application/IdentityEndorsement:
  AND('Org1MSP.peer', 'Org2MSP.peer')

3. Use Access Control

func (c *DegreeContract) IssueDegree(ctx contractapi.TransactionContextInterface, degreeID, userID string) error {

    // Get client identity
    clientID, err := ctx.GetClientIdentity().GetID()
    if err != nil {
        return fmt.Errorf("failed to get client identity")
    }

    // Only universities can issue degrees
    if !strings.HasSuffix(clientID, "university") {
        return fmt.Errorf("only universities can issue degrees")
    }

    // Proceed with issuance...
    return nil
}

Error Handling: The Right Way

Here’s a robust pattern for handling cross-chaincode invocations:

// Define custom errors
type InvocationError struct {
    ChaincodeID string
    Function    string
    Reason      string
    Status      int32
}
func (e *InvocationError) Error() string {
    return fmt.Sprintf("Failed to invoke %s.%s: %s (status %d)", 
        e.ChaincodeID, e.Function, e.Reason, e.Status)
}
// Reusable invocation with proper error handling
func (c *DegreeContract) safeInvokeIdentity(
    ctx contractapi.TransactionContextInterface,
    function string,
    args ...string,
) ([]byte, error) {

    invokeArgs := make([][]byte, 0)
    invokeArgs = append(invokeArgs, []byte(function))
    for _, arg := range args {
        invokeArgs = append(invokeArgs, []byte(arg))
    }

    response := ctx.GetStub().InvokeChaincode(
        "identity-chaincode",
        invokeArgs,
        "mychannel",
    )

    if response.Status != 200 {
        return nil, &InvocationError{
            ChaincodeID: "identity-chaincode",
            Function:    function,
            Reason:      response.Message,
            Status:      response.Status,
        }
    }

    if response.Payload == nil {
        return nil, &InvocationError{
            ChaincodeID: "identity-chaincode",
            Function:    function,
            Reason:      "empty payload received",
            Status:      500,
        }
    }

    return response.Payload, nil
}
// Usage
func (c *DegreeContract) IssueDegree(ctx contractapi.TransactionContextInterface, userID string) error {
    payload, err := c.safeInvokeIdentity(ctx, "CheckUserExists", userID)
    if err != nil {
        // Detailed error information for debugging
        return fmt.Errorf("validation error: %w", err)
    }

    userExists := string(payload) == "true"
    if !userExists {
        return fmt.Errorf("user %s not found in identity system", userID)
    }

    return nil
}

Wrapping Up: The Takeaways

Key Points:

  1. Cross-chaincode invocation via InvokeChaincode() is the professional way to share functions between chaincodes.
  2. Always verify:
  • Chaincode names match exactly (case-sensitive)
  • Both chaincodes exist on the same channel
  • Response status is 200 before processing payload
  • Payload is not nil

Chaincode names match exactly (case-sensitive)

  • Chaincode names match exactly (case-sensitive)
  • Response status is 200 before processing payload
  • Payload is not nil
  1. Understand the limitations:
  • Not atomic (each invocation is a separate transaction)
  • Requires both chaincodes to be endorsed
  • Introduces slight latency (acceptable for most use cases)
  1. Think about architecture — If a function is truly shared, consider whether it should be a separate microservice instead of a chaincode.
  2. This approach keeps your chaincodes modular, independent, and scalable — exactly what you need for production consortium blockchains.
  3. Handle errors gracefully — Use the error handling pattern shown above.

What’s Next?

Once you’ve mastered cross-chaincode invocation, consider exploring:

  • Event-driven architecture with Fabric events for loose coupling
  • Private data collections for sensitive information
  • Custom endorsement policies for complex governance

Happy coding on Fabric!


메타데이터
post_id
0877403dfd3d
slug
calling-functions-between-hyperledger-fabric-chaincodes-the-right-way-0877403dfd3d
url
https://coinsbench.com/calling-functions-between-hyperledger-fabric-chaincodes-the-right-way-0877403dfd3d
canonical_url
https://coinsbench.com/calling-functions-between-hyperledger-fabric-chaincodes-the-right-way-0877403dfd3d
author_url
https://medium.com/@muhammadtalha1
status
ok
fetched_at
2026-06-17 08:20:12