Knowledge Bank

Chaincode development

A complete guide to writing, deploying, and managing chaincode for Hyperledger Fabric networks

Introduction to chaincode

Chaincode is the smart contract implementation in Hyperledger Fabric. It defines the business logic that runs on a Fabric network and is responsible for reading and writing data to the distributed ledger.


Unlike Ethereum-based smart contracts, which run on a global public chain, chaincode runs in a permissioned network and is executed by selected endorsing peers. It is deployed in isolated Docker containers and communicates with the Fabric peer nodes through well-defined interfaces.


Chaincode allows organizations in a consortium to define rules for asset exchange, access control, regulatory checks, and other workflows using trusted code. It is executed deterministically and only changes the ledger when transaction endorsement policies are met.

Language support

Chaincode can be written in several programming languages, each offering the same functionality through different SDKs.


Currently supported languages include:

  • Go
  • JavaScript (Node.js)
  • Java

The Go language is most commonly used for production-grade chaincode due to its performance and concurrency features. Node.js is preferred for rapid prototyping or when integrating with existing JavaScript-based applications. Java is used in regulated environments where strict typing and object modeling are beneficial.

Chaincode lifecycle overview

The chaincode lifecycle defines the steps required to install, approve, and commit chaincode to a Fabric channel.


The lifecycle process is decentralized and allows each organization to participate in chaincode governance. The high-level steps are:

  • Package the chaincode
  • Install the chaincode on peers
  • Approve the chaincode definition for the channel
  • Commit the chaincode definition to the channel
  • Initialize the chaincode (optional)

Each of these steps is executed using the peer CLI or Fabric SDKs. All actions are recorded on the blockchain and can be audited by members of the consortium.

Project structure

A chaincode project typically consists of:

  • Source code files (.go, .js, or .java)
  • A go.mod file (for Go chaincode) or package.json (for Node.js)
  • Dependency modules or imports
  • A defined Init or InitLedger function
  • Business logic functions for create, read, update, and delete operations
  • Utility and helper functions for serialization and validation

For Go-based chaincode, the standard layout includes a main.go or chaincode.go entry point. This registers the chaincode and invokes the shim interface.


Node.js chaincode has an entry file like index.js or chaincode.js, which sets up the contract classes using the Fabric contract API.

Key interfaces

In Go, chaincode implements the Chaincode interface provided by the Fabric shim package. This interface includes two methods:

  • Init for initialization when the chaincode is instantiated
  • Invoke for handling all other function calls

In newer chaincode implementations using the contract API, developers define contract classes with named transaction functions. This approach supports modularity and multiple logical contracts in one chaincode.

type SmartContract struct {
}
 
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
    // initialization logic
}
 
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, value string) error {
    // asset creation logic
}

This structure improves clarity, testing, and integration with Fabric’s access control and endorsement systems.

Writing chaincode functions

Chaincode functions define how a Fabric network processes input data, verifies conditions, and updates the ledger state.


Each function receives a transaction context, which provides access to APIs for reading and writing the world state, retrieving transaction metadata, and verifying identities.

A typical chaincode function follows this flow:

  • Read input parameters using the function signature
  • Perform validation on inputs
  • Query or modify the world state using key-value operations
  • Return success or error based on logic outcomes

The function must be deterministic and must not depend on external state, time, or randomness. All peers must reach the same result independently for endorsement to succeed.

func (s *SmartContract) CreateItem(ctx contractapi.TransactionContextInterface, id string, name string) error {
    exists, err := s.ItemExists(ctx, id)
    if err != nil {
        return err
    }
    if exists {
        return fmt.Errorf("item %s already exists", id)
    }
 
    item := Item{
        ID:   id,
        Name: name,
    }
    itemJSON, err := json.Marshal(item)
    if err != nil {
        return err
    }
 
    return ctx.GetStub().PutState(id, itemJSON)
}

In this example, the function checks for duplicates, constructs a new item, marshals it into JSON, and writes it to the ledger.

Reading and writing world state

Fabric maintains a key-value database known as the world state. Each chaincode function can read and write to this store using the stub interface.

Common operations include:

  • GetState(key) to retrieve a value by key
  • PutState(key, value) to write or update a key-value pair
  • DelState(key) to delete a key
  • GetStateByRange(start, end) to iterate over a key range
  • GetQueryResult(query) for CouchDB rich queries

Data is stored as byte arrays and usually encoded in JSON for compatibility. Developers should define clear entity structures and handle serialization explicitly.

itemJSON, err := ctx.GetStub().GetState("item1")
if err != nil || itemJSON == nil {
    return fmt.Errorf("item not found")
}
 
var item Item
err = json.Unmarshal(itemJSON, &item)
if err != nil {
    return err
}

All writes to the ledger are recorded in the transaction log, and the world state reflects the latest version of each key after transaction validation.

Using client identity and attributes

Fabric supports identity-aware chaincode execution. The client identity object provides access to the invoker’s certificate, MSP ID, and attributes.

This enables use cases such as:

  • Role-based access control
  • Certificate-based ownership validation
  • Organization-specific business logic

To access the client identity:

  • Use ctx.GetClientIdentity() in Go
  • Use ctx.clientIdentity in Node.js

Examples of identity operations:

  • GetID() returns the subject of the client certificate
  • GetMSPID() returns the organization MSP
  • GetAttributeValue(name) retrieves an attribute set in the certificate
cid, err := ctx.GetClientIdentity().GetID()
if err != nil {
    return err
}
mspid, _ := ctx.GetClientIdentity().GetMSPID()
 
if mspid != "Org1MSP" {
    return fmt.Errorf("unauthorized organization")
}

These identity checks can be combined with endorsement policies to enforce multi-organization consensus.

Error handling and validation

Chaincode must return errors for invalid transactions. Errors prevent the proposal from being endorsed or committed and maintain data integrity.

Typical validation checks include:

  • Verifying that required input parameters are present
  • Ensuring keys do not already exist before creating entities
  • Confirming keys exist before reading or updating
  • Validating that caller has permission to modify a record

Use structured error messages and proper formatting. Avoid panics or uncaught exceptions. All error messages should be deterministic and consistent across all endorsing peers.

The best practice is to define helper functions for common checks and reuse them across transaction handlers.

Emitting chaincode events

Chaincode can emit events that are captured by client applications or monitoring tools.


Events are useful for triggering off-chain workflows, synchronizing UI components, or indexing ledger activity for analytics.

An event is emitted using the SetEvent method on the chaincode stub. It includes:

  • A name string that identifies the event type
  • A payload in bytes, typically a serialized JSON object
eventPayload := map[string]string{"itemId": "123", "status": "created"}
eventJSON, _ := json.Marshal(eventPayload)
ctx.GetStub().SetEvent("ItemCreated", eventJSON)

Applications can subscribe to events using the Fabric SDK and filter by event name. Events are recorded in the block that commits the transaction and are part of the transaction receipt.

Events do not modify ledger state and should not be used as the sole source of truth. Their purpose is to notify off-chain systems, not to enforce logic.

Chaincode initialization

Chaincode may include an optional initialization function that is invoked once when the chaincode is committed to a channel.


This function can perform setup tasks such as:

  • Seeding initial records
  • Setting ownership
  • Registering system-level settings

Initialization must be explicitly requested during chaincode invocation using the --isInit flag or its SDK equivalent.

Example initialization function:

func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
    items := []Item{
        {ID: "item1", Name: "Pen"},
        {ID: "item2", Name: "Notebook"},
    }
 
    for _, item := range items {
        itemJSON, _ := json.Marshal(item)
        ctx.GetStub().PutState(item.ID, itemJSON)
    }
    return nil
}

This method is called only once and is not part of regular transaction flow. If initialization is skipped or fails, the chaincode remains inactive.

Endorsement policies

An endorsement policy defines which peers must approve a transaction before it can be committed to the ledger.


Chaincode logic enforces application-level rules, while endorsement policies enforce organizational-level trust and validation.

Policies are configured during the chaincode definition phase and use logical conditions like:

  • OR('Org1MSP.peer','Org2MSP.peer')
  • AND('Org1MSP.peer','Org2MSP.peer')
  • Custom signature policies with nested conditions

These rules determine which endorsing peers must sign off on a proposal. If the required number of signatures is not collected, the transaction fails endorsement.

The endorsement policy ensures that no single organization can unilaterally update the ledger. It also enables multi-party workflows where different participants must validate the action.

Working with private data

Hyperledger Fabric allows chaincode to read and write private data collections.


Private data is not stored on the public ledger. Instead, it is distributed only to authorized peers and stored in a separate private database.

This feature supports use cases where sensitive information must be hidden from certain members of the network while still being verifiable.

Key methods for private data:

  • GetPrivateData(collection, key)
  • PutPrivateData(collection, key, value)
  • DelPrivateData(collection, key)
order := Order{ID: "order1", total: 100}
orderJSON, _ := json.Marshal(order)
ctx.GetStub().PutPrivateData("OrderCollection", "order1", orderJSON)

Collections are defined in the chaincode configuration file collections-config.json and include:

  • Collection name
  • Member organizations
  • Endorsement policy
  • Required and maximum peer counts

Private data can also be used with hashed reads and transient data inputs, enabling zero-knowledge-style logic and selective disclosure.

Access to private data is enforced at the peer level. Unauthorized peers do not receive the data and cannot query it through chaincode.

Testing chaincode

Testing chaincode is critical for ensuring correctness, security, and reliability before deployment.


Tests can be written using standard unit testing frameworks for the target language. In Go, the testing package is used to simulate chaincode transactions and verify expected behavior.

Key testing strategies include:

  • Unit tests for transaction functions using mocked contexts
  • Integration tests using Fabric test networks
  • End-to-end scenario tests with CLI or SDK interactions

Mock objects simulate the chaincode stub and transaction context. This allows developers to control inputs and check function outputs without running a full Fabric network.

Example test in Go:

func TestCreateItem(t *testing.T) {
    ctx := new(MockTransactionContext)
    stub := new(MockChaincodeStub)
    ctx.On("GetStub").Return(stub)
 
    cc := new(SmartContract)
    err := cc.CreateItem(ctx, "item1", "Laptop")
    assert.NoError(t, err)
}

Fabric also provides sample test networks using Docker Compose and scripts to simulate channel creation, peer joining, and chaincode deployment.

Packaging chaincode

Before deployment, chaincode must be packaged into a compressed archive format.


Packaging involves:

  • Creating a folder with the chaincode source and dependencies
  • Using the peer CLI to generate a .tar.gz archive
  • Assigning a label that includes version and metadata

Packaging command:

peer lifecycle chaincode package mycc.tar.gz --path ./chaincode/ --lang golang --label mycc_1

The label must be unique for each version and is used to identify the chaincode package during installation and approval.

Installing and approving chaincode

Once packaged, the chaincode must be installed on all endorsing peers and approved by all required organizations.

Installation command:

peer lifecycle chaincode install mycc.tar.gz

After installation, each peer returns a package ID that will be used during approval.

Approval command:

peer lifecycle chaincode approveformyorg --channelID mychannel --name mycc --version 1 --sequence 1 --package-id <PACKAGE_ID> --init-required

Each organization must run this command and commit the approval to the channel.

Committing chaincode

After all required approvals, the chaincode is committed to the channel using the following command:

peer lifecycle chaincode commit --channelID mychannel --name mycc --version 1 --sequence 1 --init-required

This step activates the chaincode and allows it to begin processing transactions.

If the chaincode includes an initialization function, it must be invoked with the --isInit flag:

peer chaincode invoke --channelID mychannel --name mycc -c '{"function":"InitLedger","Args":[]}' --isInit

Committing the chaincode broadcasts the definition to all peers in the channel and enables consistent execution.

Upgrading chaincode

Chaincode upgrades are handled by repeating the lifecycle steps with a higher sequence number.


To upgrade:

  • Modify the source code
  • Repackage the chaincode with a new label
  • Install the new package on all peers
  • Approve the new definition with --sequence incremented
  • Commit the new definition to the channel

This enables version-controlled deployment and supports backward-compatible changes.

Upgrade scenarios may include:

  • Adding new functions
  • Changing endorsement policy
  • Modifying access control logic
  • Migrating state formats

Developers must preserve storage layout and state compatibility across upgrades. It is also recommended to document all changes and test thoroughly in a staging environment.

Chaincode deployment strategies

In production networks, chaincode should be deployed using controlled CI/CD pipelines.


Best practices for deployment include:

  • Automating package generation and installation steps
  • Using version control to track chaincode changes
  • Storing deployment artifacts and configurations securely
  • Performing dry runs on test channels
  • Applying environment-specific parameters for each organization

Multi-org deployment requires coordination to ensure that all approvals are collected and that no inconsistent versions exist in the network.

Deployment logs, peer responses, and chaincode events should be monitored to verify successful rollout.

Multi-contract chaincode design

Chaincode can contain multiple logical contracts within a single package.


This is useful when building complex applications where multiple domains or entities must be managed independently, such as in a marketplace with users, products, and transactions.

Each contract is defined as a separate class and registered using the Fabric contract API. Contracts share the same chaincode but have isolated namespaces for better modularity.

Example:

type UserContract struct {
    contractapi.Contract
}
 
type ProductContract struct {
    contractapi.Contract
}
 
func main() {
    chaincode, err := contractapi.NewChaincode(new(UserContract), new(ProductContract))
    if err != nil {
        panic(err)
    }
 
    if err := chaincode.Start(); err != nil {
        panic(err)
    }
}

Clients invoke specific contracts using the format ContractName:FunctionName. This pattern enables structured development and simplifies logic segregation across modules.

Ledger state migration

When upgrading chaincode or modifying data structures, state migration may be required.


This process involves reading old data formats, transforming them to the new schema, and saving updated versions to the ledger.

Migration can be performed:

  • Automatically during initialization of the new chaincode version
  • Manually using a migration function triggered by an admin

Best practices for migration:

  • Maintain backward compatibility for a defined period
  • Validate data before overwriting
  • Log migrated keys and results
  • Use a dry-run mode before full execution
func (s *SmartContract) MigrateState(ctx contractapi.TransactionContextInterface) error {
    resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
    if err != nil {
        return err
    }
    defer resultsIterator.Close()
 
    for resultsIterator.HasNext() {
        response, err := resultsIterator.Next()
        if err != nil {
            return err
        }
 
        var oldRecord OldItem
        err = json.Unmarshal(response.Value, &oldRecord)
        if err != nil {
            return err
        }
 
        newRecord := NewItem{ID: oldRecord.ID, Label: oldRecord.Name}
        newJSON, _ := json.Marshal(newRecord)
        ctx.GetStub().PutState(newRecord.ID, newJSON)
    }
    return nil
}

State migration must be tested extensively to prevent corruption or data loss.

Performance optimization

Efficient chaincode execution ensures faster transaction endorsement and lower peer load.


To improve performance:

  • Use simple and direct key-value access patterns
  • Minimize writes and avoid unnecessary PutState calls
  • Cache intermediate results in memory where possible
  • Avoid large objects and excessive JSON nesting
  • Use indexed keys for fast range queries
  • Avoid heavy use of private data unless needed

Complex filtering should be done in the client application. Chaincode should serve as a deterministic validator and not as a data processing layer.

For CouchDB-based networks, rich queries should be tested for index coverage and speed. Index definitions can be added to the collection configuration for better performance.

Chaincode logging and auditability

Chaincode logs help with debugging, compliance, and transaction tracing.


Logging is supported through standard output and is captured by peer containers.

Use descriptive logs to trace function entry, key operations, and errors. Avoid logging sensitive data or large payloads in production.

In Go:

fmt.Printf("Creating item: %s\n", item.ID)

In Node.js:

console.log(`Creating item: ${itemID}`);

Chaincode operations are also recorded in transaction logs and can be queried using:

  • Block explorer tools
  • SDK query APIs
  • Peer CLI for history inspection

Audit trails include:

  • Proposal identities
  • Endorsing organizations
  • Read and write sets
  • Time of transaction
  • Chaincode version used

These features allow organizations to verify compliance, trace business activity, and investigate disputes.

Chaincode development summary

Chaincode enables secure, decentralized business logic in Hyperledger Fabric networks.


Its deterministic nature, access control capabilities, and modular architecture make it ideal for enterprise applications in finance, supply chain, healthcare, and more.

Throughout this guide, we have covered:

  • Core concepts and interfaces
  • Writing and testing transaction logic
  • World state management and identity enforcement
  • Event emission and chaincode lifecycle operations
  • Deployment, upgrades, and migration
  • Performance tuning and audit mechanisms

Successful chaincode projects follow a disciplined approach, including version control, peer review, CI pipelines, and thorough testing.


With the right patterns and tooling, chaincode becomes a powerful foundation for trusted workflows and collaborative networks.