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…

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:
- Digital Identity Chaincode — Validates users, manages email/phone verification
- Degree Verification Chaincode — Issues and verifies degrees
Both need to validate email addresses and check if a user exists. You have three choices:
- Copy-paste the functions into both chaincodes (bad)
- Share files directly using relative imports like
require('../common/validators.js')(worse) - 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, °ree); 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:
- Cross-chaincode invocation via
InvokeChaincode()is the professional way to share functions between chaincodes. - 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
- 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)
- Think about architecture — If a function is truly shared, consider whether it should be a separate microservice instead of a chaincode.
- This approach keeps your chaincodes modular, independent, and scalable — exactly what you need for production consortium blockchains.
- 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