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) orpackage.json
(for Node.js) - Dependency modules or imports
- A defined
Init
orInitLedger
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 instantiatedInvoke
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.
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.
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 keyPutState(key, value)
to write or update a key-value pairDelState(key)
to delete a keyGetStateByRange(start, end)
to iterate over a key rangeGetQueryResult(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.
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 certificateGetMSPID()
returns the organization MSPGetAttributeValue(name)
retrieves an attribute set in the certificate
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
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:
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)
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:
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:
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:
After installation, each peer returns a package ID that will be used during approval.
Approval command:
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:
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:
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:
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
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:
In Node.js:
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.