Bitcoin’s launch on 3 January 2009 set trends like digital assets and digital currencies into motion. Since then, more blockchains like Ethereum and Solana have been created. Despite having different features and use cases, these blockchains have one thing in common: they were designed to operate democratically without regulators. Therefore, this model is not suitable for regulated industries, in which data must be kept confidential and shared between trusted parties. For this reason, private blockchains exist.
A private blockchain is a permissioned blockchain that contains entities called network operators. These control the network and are able to configure permissions and access the controls of the other nodes. To maintain privacy and trust, only the entities participating in a transaction will have knowledge of it.
A few examples of digital platforms that utilize private blockchains include Hyperledger Fabric, Ripple, and R3’s Corda. In this article, we’ll explore Corda, learning how to create CorDapps. Let’s get started!
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Corda is a permissioned peer-to-peer (P2P) distributed ledger technology (DLT) that facilitates the development of applications in regulated markets. With Corda, parties may freely discover and transact with each other in a single, open network while having confidence in the identification of network participants.
In the long run, Corda aspires to be a shared worldwide distributed ledger. To do so, the many solutions that use Corda software must adhere to a set of common standards and criteria. Some of these criteria, often known as end-state principles, are as follows:
CorDapp is a shorthand for Corda distributed application. CorDapps are distributed applications that run on the Corda node. The following are the key components of a CorDapp:
We’ll create a CorDapp to model the issuance of tokens on the Corda blockchain. Our CorDapp will keep track of the issuer of the token, the holder, and the amount being issued.
There are four pieces of required software for CorDapp development:
To set up our CorDapp, first, clone the Corda Java template from their GitHub repo. Open cordapp-template-java in any IDE of your choice. I’ll use IntelliJ IDE.
To reiterate, states are immutable and keep track of data between transactions. Our CorDapp is going to have a set of attributes that we‘ll store on our state, like issuer, holder, and amount.
Navigate to /contracts/src/main/java/com/template/states and create a new Java class called TokenState:
package com.template.states
import com.template.contracts.TokenContract;
import net.corda.core.identity.AbstractParty;
import net.corda.core.contracts.BelongsToContract;
import net.corda.core.contracts.ContractState;
import net.corda.core.identity.Party;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
@BelongsToContract(TokenContract.class)
public class TokenState implements ContractState {
private Party issuer;
private Party holder;
private int amount;
public TokenState(Party issuer, Party holder, int amount){
this.issuer = issuer;
this.holder = holder;
this.amount = amount;
}
public Party getHolder() {
return holder;
}
public Party getIssuer() {
return issuer;
}
public int getAmount() {
return amount;
}
@NotNull
@Override
public List<AbstractParty> getParticipants() {
return Arrays.asList(issuer, holder);
}
}
In the code above, we create a class called TokenState that inherits from the ContractState class. The ContractState class tells Corda that we are implementing a state.
Next, we add the @BelongsToContract annotation, which establishes the relationship between a state and a contract. Without this, your state doesn’t know which contract is used to verify it. Adding this annotation triggers an error in IntelliJ because we are yet to create our TokenContract.
Then, we create three attributes, issuer, holder, and amount. The issuer and holder attributes are given a type of Party because they both represent entities on the node.
Next, we create three getter methods for each of the attributes. Finally, we create a getParticipants method that defines what parties should be aware of the transaction. In our case, we only want the issuer and the holder to be aware of the transaction.
To reiterate, contracts define the rules of how states can evolve. They make certain validations before a transaction goes through successfully. Therefore, each state is linked to a contract.
Navigate to /contracts/src/main/java/com/template/contracts and create a new Java class called TokenContract:
package com.template.contracts
import net.corda.core.contracts.CommandData;
import net.corda.core.contracts.Contract;
import net.corda.core.transactions.LedgerTransaction;
import org.jetbrains.annotations.NotNull;
import com.template.states.TokenState;
public class TokenContract implements Contract {
public static final String ID = "contracts.TokenContract";
@Override
public void verify(@NotNull LedgerTransaction tx) {
if(tx.getCommands().size() != 1) {
throw new IllegalArgumentException("expects only one command: ISSUE");
}
if(tx.getCommand(0).getValue() instanceof Commands.Issue){
throw new IllegalArgumentException("issue command expected");
}
TokenState state = (TokenState) tx.getOutput(0);
int amountIssued = state.getAmount();
if (amountIssued <= 0){
throw new IllegalArgumentException("amount must be greater than zero");
}
if(! (tx.getCommand(0).getSigners().contains(state.getIssuer().getOwningKey()))){
throw new IllegalArgumentException("transaction must be signed by issuer");
}
}
// Used to indicate the transaction's intent.
public interface Commands extends CommandData {
class Issue implements Commands {}
}
}
In the code above, we create a class called TokenContract that inherits from the Contract class. The Contract class tells Corda that we are implementing a contract.
First, we create an attribute called ID that will identify our contract when building our transaction in testing environments. The ID attribute is purely optional.
One of the methods given to us by the Contract class we inherited is the verify method, which we must override. The verify method takes transactions as input and evaluates them against defined rules. A transaction is valid if the verify method does not throw an exception.
With the code above, our contract performs the following checks:
issue command can be usedFinally, since we intend to issue the token to another party, we create a class called Issue that inherits from the Command class. The Command class is used to indicate the type of action being performed.
To reiterate, flows contain the business logic of our CorDapp. The initiator flow is run by the node initiating the transaction, which would be the issuer in our case.
Navigate to workflows/src/main/java/com/template/flows and create a new Java class called FlowInitiator:
package com.template.flows;
import co.paralleluniverse.fibers.Suspendable;
import com.bootcamp.contracts.TokenContract;
import com.bootcamp.states.TokenState;
import net.corda.core.flows.*;
import net.corda.core.identity.CordaX500Name;
import net.corda.core.identity.Party;
import net.corda.core.transactions.SignedTransaction;
import net.corda.core.transactions.TransactionBuilder;
import net.corda.core.utilities.ProgressTracker;
import net.corda.core.contracts.CommandData;
import static java.util.Collections.singletonList;
@InitiatingFlow
@StartableByRPC
public static class TokenFlowInitiator extends FlowLogic<SignedTransaction> {
private final Party owner;
private final int amount;
public TokenFlowInitiator(Party owner, int amount) {
this.owner = owner;
this.amount = amount;
}
private final ProgressTracker progressTracker = new ProgressTracker();
@Override
public ProgressTracker getProgressTracker() {
return progressTracker;
}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
/** Explicit selection of notary by CordaX500Name - argument can by coded in flows or parsed from config (Preferred)*/
final Party notary = getServiceHub().getNetworkMapCache().getNotary(CordaX500Name.parse("O=Notary,L=London,C=GB"));
// We get a reference to our own identity.
Party issuer = getOurIdentity();
/* ============================================================================
* TODO 1 - Create our TokenState to represent on-ledger tokens!
* ===========================================================================*/
// We create our new TokenState.
TokenState tokenState = new TokenState(issuer, owner, amount);
/* ============================================================================
* TODO 3 - Build our token issuance transaction to update the ledger!
* ===========================================================================*/
// We build our transaction.
TransactionBuilder transactionBuilder = new TransactionBuilder.setNotary(notary).addOutputState(tokenState).addCommand(new TokenContract.Commands.Issue(), Arrays.asList(issuer.getOwningKey(), owner.getOwningKey()));
/* ============================================================================
* TODO 2 - Write our TokenContract to control token issuance!
* ===========================================================================*/
// We check our transaction is valid based on its contracts.
transactionBuilder.verify(getServiceHub());
FlowSession session = initiateFlow(owner);
// We sign the transaction with our private key, making it immutable.
SignedTransaction signedTransaction = getServiceHub().signInitialTransaction(transactionBuilder);
// The counterparty signs the transaction
SignedTransaction fullySignedTransaction = subFlow(new CollectSignaturesFlow(signedTransaction, singletonList(session)));
// We get the transaction notarised and recorded automatically by the platform.
return subFlow(new FinalityFlow(fullySignedTransaction, singletonList(session)));
}
}
In the code above, we create a class called TokenFlowInitiator that inherits from the FlowLogic class. The FlowLogic class tells Corda that we are creating a Flow.
Then, we add two annotations, @InitiatingFlow and @StartableByRPC. The @InitiatingFlow annotation indicates that this flow is the initiating flow. On the other hand, the @StartableByRPC annotation allows RPC to start the flow.
Next, we create a variable called progressTracker that checkpoints each stage of the flow and outputs the specified messages when each checkpoint is reached in the code.
Next, we create the call method, which Corda calls when the flow is started. Inside the call method, we first create a notary and store it in a variable called notary. Since we are dealing with multiple parties, we need a notary service to reach consensus between the parties.
We get our identity and store it in a variable called issuer. Next, we create our token by creating a new instance of TokenState and pass the issuer, owner, and amount as arguments.
Then, we build our transaction proposal by creating a new instance of TransactionBuilder. We chain different methods to set our notary, add commands, and sign the transaction.
Finally, we start a FlowSession with the counterparty using the InitiateFlow method. This process enables us to send the state to the counterparty. We then call the CollectSignaturesFlow subflow to collect signatures.
The responder flow is run by the counterparty. It receives and records the transaction, then responds to the issuer’s flow by sending back an acknowledgement if the transaction was successful.
Navigate to workflows/src/main/java/com/template/flows and create a new Java class called TokenFlowResponder:
package com.template.flows;
import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.flows.*;
import net.corda.core.identity.Party;
import net.corda.core.transactions.SignedTransaction;
@InitiatedBy(TokenFlowInitiator.class)
public static class TokenFlowResponder extends FlowLogic<Void>{
//private variable
private FlowSession counterpartySession;
//Constructor
public TokenFlowResponder(FlowSession counterpartySession) {
this.counterpartySession = counterpartySession;
}
@Suspendable
@Override
public Void call() throws FlowException {
SignedTransaction signedTransaction = subFlow(new SignTransactionFlow(counterpartySession) {});
//Stored the transaction into data base.
subFlow(new ReceiveFinalityFlow(counterpartySession, signedTransaction.getId()));
return null;
}
}
In the code above, we create a class called TokenFlowResponder that inherits from the FlowLogic class. We then add the @InitiatedBy annotation and pass the TokenFlowInitiator class as an argument, telling Corda who initiated the flow.
Then, we create the call method with a subFlow that will verify the transaction and the signatures it received from the flow initiator. Optionally, we can conventionally create a new method called checkTransaction to perform a series of tests just to be safe.
To start our CorDapp, we have to first deploy it by navigating to the root of our project and running the following commands:
#Mac or Linux ./gradlew clean deployNodes #Windows gradlew.bat clean deployNodes
If our CorDapp builds successfully, it generates three nodes with the CorDapp installed on them. These can be found in the build/nodes folder.
To start the nodes and our CorDapp, run the following command from our root directory:
#Mac or Linux ./build/nodes/runnodes #Windows .\build\nodes\runnodes.bat
The code above will start a new terminal window for each node. Give each terminal some time to start, and you’ll get a welcome message on the terminal once the node is ready.
To check if our CorDapp worked successfully, we can try to start a flow by running the following command:
flow start TokenFlowInitiator owner: PartyB, amount: 100
If it is successful, you’ll get a confirmation message.
The blockchain is a game-changing technology, and regulated businesses are not left out of this change thanks to systems that use private blockchains. Blockchain projects like Corda give businesses the flexibility they need while still keeping data private. In this article, we explored getting stared with Corda, learning how to make CorDapps.
I hope you enjoyed this tutorial, and feel free to leave a comment if you have any questions.
Client-side issues that impact users’ ability to activate and transact in your apps can drastically affect your bottom line. If you’re interested in monitoring UX issues, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
Modernize how you debug web and mobile apps — start monitoring for free.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now