Skip to content

Instantly share code, notes, and snippets.

@rsrini7
Created December 22, 2025 14:16
Show Gist options
  • Select an option

  • Save rsrini7/4e560778b1295993b61a274f2a9c541f to your computer and use it in GitHub Desktop.

Select an option

Save rsrini7/4e560778b1295993b61a274f2a9c541f to your computer and use it in GitHub Desktop.
Building Enterprise Blockchain Applications with R3 Corda java

Complete Learning Guide: Building Enterprise Blockchain Applications with R3 Corda

A Comprehensive Path from Theory to Implementation

This unified learning guide combines structured video-based learning with hands-on implementation, providing a complete path to mastering R3 Corda—an open-source distributed ledger technology (DLT) designed for enterprise use cases like finance, supply chain, and healthcare.

Based on: Six-part YouTube video series by Telusko (Naveen Reddy), uploaded April-May 2021.

Prerequisites:

  • Basic Java or Kotlin knowledge (Corda runs on JVM)
  • Java 8 JDK (specifically Java 8 - Oracle, Amazon Corretto, or OpenJDK)
  • IntelliJ IDEA (Community or Ultimate Edition with Java support)
  • Git and Gradle (for project setup and builds)
  • Familiarity with blockchain basics (distributed ledgers vs. centralized databases)

Series Overview:

  • Duration: ~1-2 hours total (67+ minutes)
  • Focus: From conceptual foundations to building a complete "IOU" (I Owe You) CorDapp for debt tracking
  • Tools: Corda Open Source (version ~4.x), Gradle for builds
  • Resources:
    • Official Corda docs: corda.net
    • GitHub samples: github.com/corda/samples
    • Template: github.com/corda/cordapp-template-java

Module 1: What is Corda? Enterprise Blockchain Foundations

Video: What is Corda? | Enterprise Blockchain
Duration: ~10 minutes

Objectives

  • Understand blockchain vs. DLT fundamentals
  • Differentiate public blockchains (Bitcoin/Ethereum) from enterprise solutions
  • Learn why Corda is uniquely suited for business use cases
  • Grasp the core philosophy behind Corda's design

Key Concepts

Enterprise Focus: Private vs. Public

Unlike public blockchains (permissionless), Corda is a private, permissioned distributed ledger technology designed specifically for businesses (banks, supply chains, healthcare, etc.).

Blockchain as Distributed Database: Data stored across multiple nodes (computers) with consensus mechanisms for agreement, eliminating centralized servers controlling your data.

Aspect Public (Bitcoin/Ethereum) Private (Corda)
Privacy All transactions visible Need-to-know basis only
Fees Miner fees required No fees (known nodes)
Identity Pseudonymous addresses Verified (KYC) identities
Consensus Proof-of-Work/Stake Notary for finality
Access Open to anyone Permissioned participants
Topology Fixed network structure Customizable

Identity is Key

In public blockchains, users are anonymous or pseudo-anonymous. In Corda, every node has a known legal identity (KYC/Know Your Customer is assumed). This enables:

  • Legal accountability
  • Regulatory compliance
  • Business-to-business trust

Privacy by Design

In Bitcoin, every transaction is broadcast to every node. In Corda, data is shared on a need-to-know basis:

  • If Party A deals with Party B, Party C does not see the transaction
  • Point-to-point communication replaces broadcast
  • Each node maintains only the data it's involved in

No "Blocks" - True DLT

Technically, Corda is a DLT, not a traditional "blockchain":

  • Doesn't batch transactions into blocks
  • Confirms transactions individually in real-time
  • Chains transactions via hashes for integrity
  • Immediate finality through notary service

Notary Service

A specialized service (or node) that:

  • Prevents "double-spending"
  • Provides uniqueness consensus
  • Stamps transactions for immediate finality
  • Doesn't see transaction data (privacy preserved)

Origin & Development

  • Developed by R3 (founded 2014 as bank consortium)
  • Open-source core with enterprise edition available
  • Smart contracts written in Kotlin/Java (CorDapps)

Steps and Examples

1. Grasp Centralized vs. Distributed Architecture:

  • Centralized: Twitter stores all tweets on one server; controls all data
  • Distributed: Blockchain replicates data across nodes; consensus resolves conflicts without central authority

2. Transaction Flow in Corda:

Step 1: Parties propose transaction (e.g., A sells widget to B)
Step 2: Both parties sign digitally
Step 3: Notary validates uniqueness and stamps for finality
Step 4: Transaction stored privately on A and B's ledgers only

Example: Supply Chain Privacy

  • Screen manufacturer (A) ships components to phone assembler (B)
  • Only A and B see shipment details, pricing, and quantities
  • Notary ensures components aren't double-shipped
  • Competitor (C) sees nothing about the transaction

Example: Inter-Company Database Problem

  • Three firms track a shipment in separate databases → discrepancies arise
  • Corda Solution: Shared ledger syncs only relevant data
    • Firm A and B see shipment details
    • Firm C (uninvolved) sees nothing

Practice Exercises

  1. Compare Corda to Ethereum for a banking settlement use case: List 3 pros and 3 cons for each
  2. Sketch a transaction diagram for a private loan between two financial institutions
  3. Explain why a hospital network would choose Corda over Bitcoin for patient records

Common Pitfalls

  • Mistake: Thinking Corda is a "public blockchain" — it's permissioned DLT
  • Mistake: Overlooking the notary's role — it verifies uniqueness but doesn't see transaction data
  • Mistake: Confusing anonymity with accountability — Corda uses known, legal identities

Module 2: More About Corda - Architecture Deep Dive

Video: More about Corda | R3
Duration: ~8 minutes

Objectives

  • Explore permissioned networks and customizable topology
  • Understand notaries and their role in preventing double-spending
  • Differentiate DLT from blockchain terminology
  • Identify specific enterprise pain points Corda solves

Key Concepts

Permissioned Network Architecture

  • Only authorized nodes (known companies/organizations) can join
  • Customizable network topology based on business needs
  • Network Map Service: Maps participant names to IP addresses for discovery
  • No mining or cryptocurrency incentives needed

Privacy on Need-to-Know Basis

  • No gossiping or broadcasting
  • Point-to-point sharing between transaction parties only
  • Each node maintains its own database (ledger) containing only relevant data
  • Selective disclosure based on business relationships

Notary Deep Dive

Think of notaries like real estate notaries:

  • Trusted entity that prevents double-spending
  • Validates that input states haven't been consumed before
  • Provides timestamp and uniqueness consensus
  • Critical: Does NOT see transaction details (privacy preserved)

Notary Validation Process:

Step 1: Transaction proposes to consume a state (e.g., transfer phone ownership)
Step 2: Notary checks if that state is still unspent in its record
Step 3: If unique and valid, notary stamps the transaction
Step 4: State marked as consumed; cannot be reused

DLT vs. Blockchain Clarification

  • DLT (Distributed Ledger Technology): Broader category
    • Distributed storage across nodes
    • Immutable record-keeping
    • Consensus mechanisms
  • Blockchain: Specific type of DLT
    • Adds blocks and chain structure
    • Groups transactions into blocks

Analogy: Not all toothpastes are Colgate, but all Colgate products are toothpaste. Similarly, not all DLTs are blockchains, but all blockchains are DLTs.

Corda is DLT, not traditional blockchain because:

  • No blocks batching transactions
  • Real-time individual transaction confirmation
  • Different consensus model

Enterprise Problem Solved

Traditional Challenge: Multiple organizations maintain separate databases:

  • Company A uses SQL Server
  • Company B uses Oracle
  • Company C uses MySQL
  • Result: Data inconsistencies, reconciliation overhead, disputes

Corda Solution:

  • Shared ledger synchronized automatically
  • Each party sees only their relevant data
  • No central database authority
  • Immutable audit trail
Feature Traditional Databases Corda DLT
Data Synchronization Manual reconciliation Automatic
Single Truth Multiple versions Single shared view
Privacy N/A (separate DBs) Need-to-know basis
Audit Trail Per-organization Immutable shared
Trust Requirement Central authority Cryptographic

Steps and Examples

1. Build a Permissioned Network (Conceptual):

Step 1: Identify participants (e.g., Apple, Samsung, Dell nodes)
Step 2: Register with Network Map Service (maps name → IP address)
Step 3: Establish point-to-point communication channels
Step 4: Deploy CorDapps to define business logic

2. Asset History Tracking:

Phone Sale Chain:
- Initial State: Phone owned by Company A
- Transaction 1: A sells to B (notary validates)
- Transaction 2: B sells to C (notary validates)
- Result: Immutable chain A → B → C (privately tracked)

Example: Healthcare Records Sharing

  • Hospital A treats patient, creates record
  • Patient moves to Hospital B
  • Only Hospitals A and B see patient's full record
  • Insurance company (C) sees only billing-relevant data
  • Privacy maintained while ensuring continuity of care

Practice Exercises

  1. Diagram a 3-node network for secure document sharing between law firms
  2. Explain why Corda avoids miners and how this benefits enterprises
  3. Compare the cost structure of Ethereum vs. Corda for 1000 daily transactions

Common Pitfalls

  • Mistake: Confusing anonymity with accountability — Corda requires known identities for legal/regulatory compliance
  • Mistake: Assuming all participants see all data — privacy is selective
  • Mistake: Thinking notaries are a bottleneck — multiple notaries can coexist for scalability

Module 3: Corda Key Concepts - Technical Architecture

Video: Corda Key Concepts
Duration: ~12 minutes

Objectives

  • Master core components: Nodes, ledgers, states, contracts, flows
  • Understand consensus mechanisms (validity + uniqueness)
  • Learn the UTXO model and state management
  • Grasp how automation works through flows

Key Concepts - The Building Blocks

Concept Description
Node Computer running Corda software; represents an organization (e.g., a bank); holds private ledger
Network Collection of nodes that can communicate; uses Network Map for discovery
Ledger Per-node storage of relevant states and transaction history; no single central ledger
State Immutable object representing a fact at a specific point in time (e.g., "Alice owes Bob $10"); never changed, only consumed
Transaction Mechanism to update ledger; atomic change that consumes input states and creates output states
Contract Legal/business rules written in Java/Kotlin that verify transaction validity (e.g., "Cannot lend negative money")
Command Indicates the intent of a transaction (e.g., "Create", "Transfer", "Settle")
Notary Specialized service preventing double-spending; provides uniqueness consensus
Flow Automated script handling multi-party agreement process; coordinates communication between parties
Oracle External data provider (e.g., exchange rates, weather data) for smart contracts

Node - Enterprise Representative

  • Physical or virtual machine running Corda
  • Has legal identity (X.500 name: Organization, Location, Country)
  • Runs CorDapps (business applications)
  • Maintains vault (database) of states it's involved in
  • Example: "O=BankOfAmerica, L=New York, C=US"

Network - Communication Infrastructure

  • Point-to-point connections between nodes
  • Network Map Service provides:
    • Node discovery (name → network address)
    • Notary listing
    • Network parameters
  • Customizable topology per business needs

Ledger - Private, Distributed Storage

Key insight: There is no single central ledger

  • Each node maintains its own database
  • Contains only states the node is involved in (privacy)
  • Cryptographically linked for integrity
  • Synchronized through transactions and flows

State - Immutable Facts

Philosophy: States are never modified; they are consumed to create new states.

State Lifecycle:

1. UNCONSUMED (Current) → Active, can be used as input
2. CONSUMED (Historical) → Archived, cannot be reused

Key Properties:

  • Participants: List of parties who should store this state
  • Linear States: Evolve over time (e.g., loan agreement)
  • Fungible States: Interchangeable (e.g., currency)

Example - Phone Ownership State:

class PhoneState {
    Party owner;        // Current owner
    String model;       // Phone model
    int value;         // Current value
    
    List<Party> getParticipants() {
        return Arrays.asList(owner);
    }
}

Transaction - State Evolution

Atomic operation that:

  1. Consumes input states (marks them as historical)
  2. Creates output states (new current facts)
  3. Executes commands (intent)
  4. Verifies against contracts (validation)

Transaction Components:

  • Inputs: References to states being consumed
  • Outputs: New states being created
  • Commands: Intent + required signers
  • Attachments: Supporting documents (PDFs, contracts)
  • Time Window: Optional validity period
  • Notary: Which notary validates uniqueness

UTXO Model (Unspent Transaction Output):

  • Similar to Bitcoin
  • Outputs from one transaction become inputs to next
  • Prevents double-spending through notary

Contract - Business Rules Enforcement

Legal agreement encoded in code:

Key Responsibilities:

  1. Verify transaction structure is valid
  2. Check business logic constraints
  3. Throw exceptions if rules violated

Example Constraints:

  • "IOU value must be positive (> 0)"
  • "Lender and Borrower cannot be the same entity"
  • "No inputs should be consumed when creating an IOU"
  • "Must have exactly one output state"
  • "Required signers must sign the transaction"

Contract Structure:

public class IOUContract implements Contract {
    public static final String ID = "com.template.IOUContract";
    
    public interface Commands extends CommandData {
        class Create implements Commands {}
        class Transfer implements Commands {}
    }
    
    @Override
    public void verify(LedgerTransaction tx) {
        // Validation logic here
        // Throw IllegalArgumentException if rules violated
    }
}

Command - Transaction Intent

Indicates what the transaction is trying to do:

  • Create: Issue new state
  • Move/Transfer: Change ownership
  • Exit/Settle: Remove state from ledger
  • Custom commands for specific business logic

Commands specify required signers:

builder.addCommand(
    new IOUContract.Commands.Create(),
    Arrays.asList(lender.getOwningKey(), borrower.getOwningKey())
);

Notary - Uniqueness Consensus

Critical Role: Prevents double-spending

How It Works:

  1. Transaction submitted to notary
  2. Notary checks if input states are unspent
  3. If valid: Signs and timestamps
  4. If already spent: Rejects with conflict details

Types:

  • Validating Notary: Sees transaction details, validates contracts
  • Non-Validating Notary: Only checks uniqueness (privacy preserved)

Flow - Process Automation

Scripts that automate multi-step, multi-party processes:

Flow Types:

  • @InitiatingFlow: Starts the process
  • @InitiatedBy: Responds to initiating flow
  • @StartableByRPC: Can be triggered via API

Typical Flow Steps:

  1. Build transaction
  2. Verify locally
  3. Sign transaction
  4. Collect counterparty signatures
  5. Notarize
  6. Finalize (record to all vaults)

Example - Multi-Party House Purchase:

Buyer Flow:
  ↓
1. Initiates transaction
  ↓
2. Bank verifies funds available
  ↓
3. Insurance company confirms coverage
  ↓
4. Seller signs transfer
  ↓
5. Notary validates and timestamps
  ↓
6. All parties record final state

Consensus - Two Types

  1. Validity Consensus:

    • All participants verify contract rules
    • All required parties sign
    • Happens before notarization
  2. Uniqueness Consensus:

    • Notary ensures states haven't been double-spent
    • Single authority for finality
    • Happens at notarization

Oracle - External Data Provider

Brings off-chain data on-chain:

  • Currency exchange rates
  • Weather data
  • Stock prices
  • Identity verification

Usage Pattern:

1. Smart contract needs external data
2. Query oracle for signed data
3. Include oracle signature in transaction
4. Contract verifies oracle signature

Steps and Examples

1. State Management Example:

Step 1: Create initial state
  → PhoneState(owner=A, model="iPhone", value=1000)
  
Step 2: Transaction consumes it
  → Input: PhoneState(owner=A...)
  → Output: PhoneState(owner=B, model="iPhone", value=950)
  
Step 3: Old state archived
  → State A-to-A marked CONSUMED
  → State A-to-B marked UNCONSUMED

2. Transaction with Contract Example:

// Build transaction
TransactionBuilder txBuilder = new TransactionBuilder(notary);
txBuilder.addInputState(oldPhoneState);
txBuilder.addOutputState(newPhoneState, PhoneContract.ID);
txBuilder.addCommand(
    new PhoneContract.Commands.Sell(),
    Arrays.asList(seller.getOwningKey(), buyer.getOwningKey())
);

// Contract verify checks:
// - Price not increased
// - Phone not damaged
// - Both parties signed

3. Complete Phone Sale Flow:

Initial: Phone owned by C, value $500

Transaction:
  Inputs:  [Phone owned by C, $500]
  Outputs: [Phone owned by D, $450]
  Command: Sell
  Contract Checks:
    ✓ New price ≤ original price
    ✓ Both C and D signed
    ✓ No damage reported
  
Notary: Validates C hasn't sold phone to anyone else
Result: D owns phone, C's ownership consumed

Practice Exercises

  1. Define a state for a medical record transfer including patient consent
  2. Outline a complete flow for an interbank payment with compliance checks
  3. Design a contract for a supply chain shipment with quality verification
  4. Explain how UTXO model prevents double-spending without seeing all transactions

Common Pitfalls

  • Mistake: Forgetting to implement getParticipants() in states — this defines who stores the state
  • Mistake: Trying to modify states directly — always consume and create new
  • Mistake: Overlooking flows for complex transactions — don't hardcode multi-party processes
  • Mistake: Not understanding the difference between validity and uniqueness consensus
  • Mistake: Thinking the notary sees all transaction data (non-validating notaries don't)

Module 4: Environment Setup & Exploring Sample CorDapp

Video: Exploring Cordapp
Duration: ~15 minutes

Objectives

  • Set up complete Corda development environment
  • Clone and explore a sample CorDapp
  • Deploy and run a multi-node network
  • Interact via RPC shell and REST APIs
  • Verify privacy features in action

Key Concepts

CorDapp (Corda Distributed Application)

JVM-based applications running on Corda network:

  • Written in Java or Kotlin
  • Deployed to nodes as JAR files
  • Contains contracts (rules) and workflows (flows)

Project Structure

cordapp/
├── contracts/          # Contract code (rules)
│   ├── src/main/java/
│   └── build.gradle
├── workflows/          # Flow code (processes)
│   ├── src/main/java/
│   └── build.gradle
├── clients/            # Optional REST API/UI
└── build.gradle        # Main build configuration

IOU Example Context

IOU = "I Owe You" debt token

  • Lender: Party who lent money
  • Borrower: Party who owes money
  • Value: Amount owed
  • Linear state that tracks debt obligations

Flows in IOU

  • @InitiatingFlow: Lender starts IOU creation
  • @InitiatedBy: Borrower receives and accepts
  • @StartableByRPC: Callable via command line or API

RPC (Remote Procedure Call)

Interface for interacting with nodes:

  • Command-line shell (CRaSH)
  • REST API
  • Custom clients

Steps and Examples

Part A: Environment Setup

1. Install Prerequisites:

# Verify Java 8
java -version
# Should show: java version "1.8.x"

# Verify Gradle (via wrapper)
./gradlew --version

# Verify Git
git --version

2. Clone Sample Project:

# Official samples repository
git clone https://github.com/corda/samples-java.git
cd samples-java/Basic/cordapp-example

# OR use template for new projects
git clone https://github.com/corda/cordapp-template-java.git

3. Open in IntelliJ IDEA:

File → Open → Select 'build.gradle' → Open as Project
Wait for Gradle sync (downloads dependencies - may take several minutes)

4. Project Configuration (build.gradle):

// Key configurations
ext.corda_release_version = '4.x'
ext.corda_gradle_plugins_version = '4.x'

// Nodes configuration
task deployNodes(type: net.corda.plugins.Cordform) {
    nodeDefaults {
        projectCordapp {
            deploy = false
        }
    }
    
    node {
        name "O=Notary,L=London,C=GB"
        notary = [validating : false]
    }
    
    node {
        name "O=PartyA,L=London,C=GB"
        p2pPort 10002
        rpcSettings {
            address("localhost:10003")
            adminAddress("localhost:10043")
        }
    }
    
    node {
        name "O=PartyB,L=New York,C=US"
        p2pPort 10005
        rpcSettings {
            address("localhost:10006")
            adminAddress("localhost:10046")
        }
    }
}

Part B: Building and Deploying

1. Build the Project:

# Windows
gradlew.bat clean build

# Mac/Linux
./gradlew clean build

2. Deploy Nodes:

# Windows
gradlew.bat deployNodes

# Mac/Linux
./gradlew deployNodes

# This creates: build/nodes/
# ├── Notary/
# ├── PartyA/
# ├── PartyB/
# └── runnodes[.bat]

3. Start the Network:

# Windows
build\nodes\runnodes.bat

# Mac/Linux
build/nodes/runnodes

# Opens separate terminal windows for each node
# Wait for: "Welcome to the Corda interactive shell"

Part C: Exploring IOUState

Code: IOUState.java

package com.template.states;

import com.template.contracts.IOUContract;
import net.corda.core.contracts.BelongsToContract;
import net.corda.core.contracts.ContractState;
import net.corda.core.contracts.LinearState;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.Party;

import java.util.Arrays;
import java.util.List;

@BelongsToContract(IOUContract.class)
public class IOUState implements ContractState, LinearState {
    // Fields
    private final int value;
    private final Party lender;
    private final Party borrower;
    private final UniqueIdentifier linearId;
    
    // Constructor
    public IOUState(int value, Party lender, Party borrower, UniqueIdentifier linearId) {
        this.value = value;
        this.lender = lender;
        this.borrower = borrower;
        this.linearId = linearId;
    }
    
    // Getters
    public int getValue() { return value; }
    public Party getLender() { return lender; }
    public Party getBorrower() { return borrower; }
    
    @Override
    public UniqueIdentifier getLinearId() { return linearId; }
    
    // Defines who stores this state
    @Override
    public List<AbstractParty> getParticipants() {
        return Arrays.asList(lender, borrower);
    }
}

Key Points:

  • @BelongsToContract: Links state to its contract
  • LinearState: Can evolve over time (vs. fungible)
  • getParticipants(): Only lender and borrower store this IOU

Part D: Exploring IOUContract

Code: IOUContract.java

package com.template.contracts;

import com.template.states.IOUState;
import net.corda.core.contracts.CommandData;
import net.corda.core.contracts.Contract;
import net.corda.core.transactions.LedgerTransaction;

import static net.corda.core.contracts.ContractsDSL.requireThat;

public class IOUContract implements Contract {
    public static final String ID = "com.template.contracts.IOUContract";
    
    @Override
    public void verify(LedgerTransaction tx) {
        // Get the command
        final CommandData commandData = tx.getCommands().get(0).getValue();
        
        if (commandData instanceof Commands.Create) {
            // Constraints for Create command
            requireThat(require -> {
                // Shape constraints
                require.using("No inputs should be consumed when issuing an IOU.",
                        tx.getInputs().isEmpty());
                require.using("There should be one output state of type IOUState.",
                        tx.getOutputs().size() == 1);
                
                // Content constraints
                final IOUState output = tx.outputsOfType(IOUState.class).get(0);
                require.using("The IOU's value must be non-negative.",
                        output.getValue() > 0);
                require.using("The lender and the borrower cannot be the same entity.",
                        !output.getLender().equals(output.getBorrower()));
                
                return null;
            });
        }
    }
    
    public interface Commands extends CommandData {
        class Create implements Commands {}
    }
}

Validation Logic:

  1. Shape: No inputs, exactly one output
  2. Value: Must be positive
  3. Parties: Lender ≠ Borrower

Part E: Exploring Flows

Code: Initiator Flow (Simplified)

@InitiatingFlow
@StartableByRPC
public class IOUFlow extends FlowLogic<SignedTransaction> {
    private final int iouValue;
    private final Party otherParty;
    
    public IOUFlow(int iouValue, Party otherParty) {
        this.iouValue = iouValue;
        this.otherParty = otherParty;
    }
    
    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        // Step 1: Get notary
        final Party notary = getServiceHub().getNetworkMapCache()
                .getNotaryIdentities().get(0);
        
        // Step 2: Create output state
        final IOUState outputState = new IOUState(
            iouValue,
            getOurIdentity(),
            otherParty,
            new UniqueIdentifier()
        );
        
        // Step 3: Build transaction
        final TransactionBuilder txBuilder = new TransactionBuilder(notary)
                .addOutputState(outputState, IOUContract.ID)
                .addCommand(new IOUContract.Commands.Create(),
                        Arrays.asList(getOurIdentity().getOwningKey(),
                                     otherParty.getOwningKey()));
        
        // Step 4: Verify transaction
        txBuilder.verify(getServiceHub());
        
        // Step 5: Sign transaction
        final SignedTransaction partSignedTx = getServiceHub()
                .signInitialTransaction(txBuilder);
        
        // Step 6: Collect counterparty signature
        FlowSession otherPartySession = initiateFlow(otherParty);
        final SignedTransaction fullySignedTx = subFlow(
                new CollectSignaturesFlow(partSignedTx,
                        Arrays.asList(otherPartySession)));
        
        // Step 7: Finalize (notarize and record)
        return subFlow(new FinalityFlow(fullySignedTx,
                Arrays.asList(otherPartySession)));
    }
}

Responder Flow:

@InitiatedBy(IOUFlow.class)
public class IOUFlowResponder extends FlowLogic<SignedTransaction> {
    private final FlowSession otherPartySession;
    
    public IOUFlowResponder(FlowSession otherPartySession) {
        this.otherPartySession = otherPartySession;
    }
    
    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        class SignTxFlow extends SignTransactionFlow {
            private SignTxFlow(FlowSession otherPartySession) {
                super(otherPartySession);
            }
            
            @Override
            protected void checkTransaction(SignedTransaction stx) {
                // Custom validation before signing
                requireThat(require -> {
                    ContractState output = stx.getTx().getOutputs().get(0).getData();
                    require.using("This must be an IOU transaction.",
                            output instanceof IOUState);
                    return null;
                });
            }
        }
        
        final SignTxFlow signTxFlow = new SignTxFlow(otherPartySession);
        final SecureHash txId = subFlow(signTxFlow).getId();
        
        return subFlow(new ReceiveFinalityFlow(otherPartySession, txId));
    }
}

Part F: Running Transactions

1. Via RPC Shell (CRaSH):

Check existing IOUs:

# In PartyA terminal
run vaultQuery contractStateType: com.template.states.IOUState

# Should show: Empty (initially)

Create IOU:

# In PartyA terminal
flow start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US"

# Output shows:
# ✓ Starting
# ✓ Generating transaction
# ✓ Verifying transaction
# ✓ Signing transaction
# ✓ Collecting signatures
# ✓ Finalising transaction
# Transaction committed with ID: [TX_HASH]

Verify Creation:

# PartyA terminal
run vaultQuery contractStateType: com.template.states.IOUState
# Shows: IOUState(value=99, lender=PartyA, borrower=PartyB)

# PartyB terminal
run vaultQuery contractStateType: com.template.states.IOUState
# Shows: Same IOU (both parties store it)

# PartyC terminal (if exists)
run vaultQuery contractStateType: com.template.states.IOUState
# Shows: Empty (privacy feature!)

2. Via REST API:

Create IOU via HTTP:

curl -X POST http://localhost:10005/api/template/create-iou \
  -H "Content-Type: application/json" \
  -d '{
    "iouValue": 100,
    "partyName": "O=PartyB,L=New York,C=US"
  }'

# Response:
# {
#   "txId": "ABCD1234...",
#   "msg": "Transaction committed successfully"
# }

Query IOUs:

curl http://localhost:10005/api/template/ious

# Response:
# [
#   {
#     "value": 100,
#     "lender": "O=PartyA,L=London,C=GB",
#     "borrower": "O=PartyB,L=New York,C=US",
#     "linearId": "..."
#   }
# ]

Practice Exercises

  1. Add PartyC as an observer; verify it cannot see IOUs between A and B
  2. Modify curl request to send IOU value of 0; observe the contract failure
  3. Create 5 IOUs with different values; query and compare vaults
  4. Try to create IOU where lender = borrower; verify the contract prevents it
  5. Implement a REST endpoint to query IOUs by minimum value

Common Pitfalls

  • Java Version: Must use Java 8 specifically — newer versions (11, 17) will fail
  • Command Syntax: Flow parameters are case-sensitive; match exactly
  • Party Names: Must use full X.500 name format: "O=PartyB,L=New York,C=US"
  • Missing Spaces: In shell commands, no spaces after colons: iouValue:99 not iouValue: 99
  • Node Startup: Wait for "Welcome to Corda" message before running commands
  • Gradle Sync: Always let IntelliJ complete Gradle sync before building

Module 5: Writing a CorDapp from Scratch - Part 1

Video: Writing a Cordapp | Part 1
Duration: ~10 minutes

Objectives

  • Start a CorDapp project from official template
  • Implement IOUState with proper structure
  • Create basic IOUContract skeleton
  • Build simple Initiator and Responder flows
  • Deploy and test the basic functionality

Key Concepts

Template Approach

Instead of writing configuration files from scratch:

  • Use official Cordapp Template from GitHub
  • Pre-configured Gradle build files
  • Proper project structure
  • Example states, contracts, flows (to be replaced)

TransactionBuilder

Core class for assembling transactions:

TransactionBuilder builder = new TransactionBuilder(notary);
builder.addOutputState(state, contractID);
builder.addCommand(command, signers);
builder.verify(serviceHub);  // Pre-check before signing

ServiceHub

Node's interface to Corda services:

  • getNetworkMapCache(): Find parties and notaries
  • signInitialTransaction(): Sign with node's key
  • getVaultService(): Query stored states

@Suspendable Annotation

Required for flow methods that pause/resume:

@Suspendable
@Override
public Void call() throws FlowException {
    // Flow logic that waits for network responses
}

Steps and Examples

Step 1: Clone Template and Setup

1. Get Template:

git clone https://github.com/corda/cordapp-template-java.git
cd cordapp-template-java

2. Open in IntelliJ:

File → Open → Select 'build.gradle'
Let Gradle sync complete (downloads dependencies)

3. Clean Up Template:

Project Structure:
├── contracts/
│   └── src/main/java/com/template/
│       ├── TemplateState.java     (DELETE or rename)
│       └── TemplateContract.java  (DELETE or rename)
├── workflows/
│   └── src/main/java/com/template/
│       └── Initiator.java         (MODIFY)
│       └── Responder.java         (MODIFY)
└── build.gradle                   (CONFIGURE)

Optional: Delete clients/ folder if not building REST API

Step 2: Create IOUState

File: contracts/src/main/java/com/template/states/IOUState.java

package com.template.states;

import com.template.contracts.IOUContract;
import net.corda.core.contracts.BelongsToContract;
import net.corda.core.contracts.ContractState;
import net.corda.core.contracts.LinearState;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.Party;

import java.util.Arrays;
import java.util.List;

/**
 * State representing an IOU (I Owe You).
 * Records that lender has lent 'value' amount to borrower.
 */
@BelongsToContract(IOUContract.class)
public class IOUState implements ContractState, LinearState {
    
    // Private fields (immutable)
    private final int value;
    private final Party lender;
    private final Party borrower;
    private final UniqueIdentifier linearId;
    
    /**
     * Constructor
     * @param value Amount owed
     * @param lender Party who lent money
     * @param borrower Party who borrowed money
     * @param linearId Unique identifier for tracking
     */
    public IOUState(int value, Party lender, Party borrower, UniqueIdentifier linearId) {
        this.value = value;
        this.lender = lender;
        this.borrower = borrower;
        this.linearId = linearId;
    }
    
    // Getters
    public int getValue() { return value; }
    public Party getLender() { return lender; }
    public Party getBorrower() { return borrower; }
    
    @Override
    public UniqueIdentifier getLinearId() { return linearId; }
    
    /**
     * Determines who stores this state on their node.
     * Both lender and borrower need a copy.
     */
    @Override
    public List<AbstractParty> getParticipants() {
        return Arrays.asList(lender, borrower);
    }
}

Key Design Decisions:

  • Immutable fields: States never change; new ones are created
  • LinearState interface: Allows tracking IOU evolution over time
  • Participants: Both parties store the IOU (privacy maintained)

Step 3: Create IOUContract (Basic)

File: contracts/src/main/java/com/template/contracts/IOUContract.java

package com.template.contracts;

import net.corda.core.contracts.CommandData;
import net.corda.core.contracts.Contract;
import net.corda.core.transactions.LedgerTransaction;

/**
 * Contract for IOU states.
 * Defines rules for creating, transferring, and settling IOUs.
 */
public class IOUContract implements Contract {
    
    // Contract ID for identification
    public static final String ID = "com.template.contracts.IOUContract";
    
    /**
     * Verification logic for transactions.
     * Checks that transaction follows business rules.
     */
    @Override
    public void verify(LedgerTransaction tx) throws IllegalArgumentException {
        // Empty for now - will add constraints in Part 2
        // This allows any transaction to pass (for initial testing)
    }
    
    /**
     * Commands define the intent of the transaction
     */
    public interface Commands extends CommandData {
        class Create implements Commands {}
        class Transfer implements Commands {}
        class Settle implements Commands {}
    }
}

Note: Verification left empty initially for testing; will add rules in Module 6.

Step 4: Create Initiator Flow

File: workflows/src/main/java/com/template/flows/Initiator.java

package com.template.flows;

import co.paralleluniverse.fibers.Suspendable;
import com.template.contracts.IOUContract;
import com.template.states.IOUState;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.flows.*;
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 java.util.Arrays;

/**
 * Initiating flow for creating an IOU.
 * Lender starts this flow to issue IOU to borrower.
 */
@InitiatingFlow
@StartableByRPC
public class Initiator extends FlowLogic<SignedTransaction> {
    
    // Parameters
    private final int iouValue;
    private final Party otherParty;
    
    // Progress tracker for UI/logging
    private final ProgressTracker progressTracker = new ProgressTracker();
    
    public Initiator(int iouValue, Party otherParty) {
        this.iouValue = iouValue;
        this.otherParty = otherParty;
    }
    
    @Override
    public ProgressTracker getProgressTracker() {
        return progressTracker;
    }
    
    /**
     * Main flow logic
     */
    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        
        // STEP 1: Get the first notary from network
        final Party notary = getServiceHub()
                .getNetworkMapCache()
                .getNotaryIdentities()
                .get(0);
        
        // STEP 2: Create the output state
        final IOUState outputState = new IOUState(
            iouValue,
            getOurIdentity(),  // We are the lender
            otherParty,        // Borrower
            new UniqueIdentifier()
        );
        
        // STEP 3: Build the transaction
        final TransactionBuilder txBuilder = new TransactionBuilder(notary);
        
        // Add output state
        txBuilder.addOutputState(outputState, IOUContract.ID);
        
        // Add command (intent + required signers)
        txBuilder.addCommand(
            new IOUContract.Commands.Create(),
            Arrays.asList(
                getOurIdentity().getOwningKey(),
                otherParty.getOwningKey()
            )
        );
        
        // STEP 4: Verify transaction locally (checks contract rules)
        txBuilder.verify(getServiceHub());
        
        // STEP 5: Sign the transaction with our key
        final SignedTransaction partiallySignedTx = 
            getServiceHub().signInitialTransaction(txBuilder);
        
        // STEP 6: Create session with counterparty
        final FlowSession otherPartySession = initiateFlow(otherParty);
        
        // STEP 7: Collect their signature
        final SignedTransaction fullySignedTx = subFlow(
            new CollectSignaturesFlow(
                partiallySignedTx,
                Arrays.asList(otherPartySession)
            )
        );
        
        // STEP 8: Finalize (notarize and record to both vaults)
        return subFlow(new FinalityFlow(
            fullySignedTx,
            Arrays.asList(otherPartySession)
        ));
    }
}

Flow Breakdown:

  1. Find Notary: Get first available notary from network
  2. Create State: Build IOUState with data
  3. Build Transaction: Add outputs and commands
  4. Verify: Pre-check contract rules
  5. Sign: Lender signs first
  6. Collect Signatures: Get borrower's signature
  7. Finalize: Notarize and distribute to vaults

Step 5: Create Responder Flow

File: workflows/src/main/java/com/template/flows/Responder.java

package com.template.flows;

import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.flows.*;
import net.corda.core.transactions.SignedTransaction;

/**
 * Responder flow for receiving IOU creation requests.
 * Borrower runs this automatically when lender initiates.
 */
@InitiatedBy(Initiator.class)
public class Responder extends FlowLogic<SignedTransaction> {
    
    private final FlowSession otherPartySession;
    
    public Responder(FlowSession otherPartySession) {
        this.otherPartySession = otherPartySession;
    }
    
    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        
        // STEP 1: Sign the transaction
        // (Basic version - just signs without checking)
        class SignTxFlow extends SignTransactionFlow {
            private SignTxFlow(FlowSession otherPartySession) {
                super(otherPartySession);
            }
            
            @Override
            protected void checkTransaction(SignedTransaction stx) {
                // Basic validation
                // Will add custom checks in Part 2
            }
        }
        
        final SignTxFlow signTxFlow = new SignTxFlow(otherPartySession);
        final SecureHash txId = subFlow(signTxFlow).getId();
        
        // STEP 2: Receive finalized transaction
        return subFlow(new ReceiveFinalityFlow(otherPartySession, txId));
    }
}

Note: Basic responder that automatically signs; will add validation in Module 6.

Step 6: Configure Build File

File: build.gradle (Node Configuration)

task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
    
    nodeDefaults {
        projectCordapp {
            deploy = false
        }
        cordapp project(':contracts')
        cordapp project(':workflows')
    }
    
    node {
        name "O=Notary,L=London,C=GB"
        notary = [validating : false]
        p2pPort 10002
        rpcSettings {
            address("localhost:10003")
            adminAddress("localhost:10043")
        }
    }
    
    node {
        name "O=PartyA,L=London,C=GB"
        p2pPort 10005
        rpcSettings {
            address("localhost:10006")
            adminAddress("localhost:10046")
        }
    }
    
    node {
        name "O=PartyB,L=New York,C=US"
        p2pPort 10008
        rpcSettings {
            address("localhost:10009")
            adminAddress("localhost:10049")
        }
    }
}

Step 7: Build and Deploy

1. Build:

# Windows
gradlew.bat clean build

# Mac/Linux
./gradlew clean build

2. Deploy Nodes:

# Windows
gradlew.bat deployNodes

# Mac/Linux
./gradlew deployNodes

# Creates: build/nodes/ with PartyA, PartyB, Notary

3. Start Network:

# Windows
build\nodes\runnodes.bat

# Mac/Linux
build/nodes/runnodes

# Wait for all nodes to show: "Welcome to Corda interactive shell"

Step 8: Test Basic Flow

In PartyA Terminal:

# Check empty vault
run vaultQuery contractStateType: com.template.states.IOUState
# Output: No states

# Create IOU
flow start Initiator iouValue: 50, otherParty: "O=PartyB,L=New York,C=US"

# Output shows progress:
# Starting
# Generating transaction
# Signing
# Collecting signatures
# Finalising
# ✓ Done

# Verify creation
run vaultQuery contractStateType: com.template.states.IOUState
# Shows: IOUState(value=50, lender=PartyA, borrower=PartyB)

In PartyB Terminal:

# Check vault
run vaultQuery contractStateType: com.template.states.IOUState
# Shows: Same IOU (both parties have it)

Practice Exercises

  1. Add a description field to IOUState for note about the debt
  2. Create a third command Cancel in the contract
  3. Implement logging in the Initiator flow using logger.info()
  4. Try creating IOU from PartyB to PartyA (reverse direction)
  5. Add a Move command and basic transfer flow

Common Pitfalls

  • Missing @Suspendable: Flow methods must have this annotation or they'll hang
  • Not Reloading Gradle: After changing build.gradle, reload project
  • Wrong Package Names: Ensure states/contracts match package declarations
  • Forgetting to Build: Must run gradlew build before deployNodes
  • Party Name Typos: X.500 names are case-sensitive and format-strict
  • Old Nodes Running: Stop previous nodes before starting new ones

Module 6: Writing a CorDapp from Scratch - Part 2

Video: Writing a Cordapp | Part 2
Duration: ~12 minutes

Objectives

  • Add comprehensive verification logic to contracts
  • Implement custom validation in responder flows
  • Use requireThat DSL for readable constraints
  • Handle multi-party signing with CollectSignaturesFlow
  • Test constraint violations and error handling

Key Concepts

requireThat DSL

Corda's Domain-Specific Language for readable constraints:

requireThat(require -> {
    require.using("Error message", condition);
    require.using("Another message", another_condition);
return null;
});

CollectSignaturesFlow

Built-in flow for gathering signatures from multiple parties:

SignedTransaction fullySignedTx = subFlow(
    new CollectSignaturesFlow(
        partiallySignedTx,
        sessions,
        CollectSignaturesFlow.tracker()
    )
);

SignTransactionFlow

Base class for custom responder validation:

class CustomSignTxFlow extends SignTransactionFlow {
    @Override
    protected void checkTransaction(SignedTransaction stx) {
        // Custom validation before signing
    }
}

Contract Verification Logic

Multiple layers of validation:

  1. Shape Constraints: Number of inputs/outputs
  2. Content Constraints: Data validity
  3. Signer Constraints: Required signatures

Steps and Examples

Step 1: Enhanced Contract Verification

File: contracts/src/main/java/com/template/contracts/IOUContract.java

package com.template.contracts;

import com.template.states.IOUState;
import net.corda.core.contracts.CommandData;
import net.corda.core.contracts.CommandWithParties;
import net.corda.core.contracts.Contract;
import net.corda.core.transactions.LedgerTransaction;

import java.security.PublicKey;
import java.util.List;
import java.util.stream.Collectors;

import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
import static net.corda.core.contracts.ContractsDSL.requireThat;

/**
 * IOU Contract with comprehensive validation rules.
 */
public class IOUContract implements Contract {
    
    public static final String ID = "com.template.contracts.IOUContract";
    
    @Override
    public void verify(LedgerTransaction tx) throws IllegalArgumentException {
        
        // Extract the command from the transaction
        final CommandWithParties<Commands> command = requireSingleCommand(
            tx.getCommands(),
            Commands.class
        );
        
        // Get the command value
        final Commands commandData = command.getValue();
        
        // Get the required signers
        final List<PublicKey> requiredSigners = command.getSigners();
        
        // Verify based on command type
        if (commandData instanceof Commands.Create) {
            verifyCreate(tx, requiredSigners);
        } else if (commandData instanceof Commands.Transfer) {
            verifyTransfer(tx, requiredSigners);
        } else if (commandData instanceof Commands.Settle) {
            verifySettle(tx, requiredSigners);
        } else {
            throw new IllegalArgumentException("Unrecognised command");
        }
    }
    
    /**
     * Verification rules for Create command
     */
    private void verifyCreate(LedgerTransaction tx, List<PublicKey> signers) {
        requireThat(require -> {
            // ===== SHAPE CONSTRAINTS =====
            require.using("No inputs should be consumed when issuing an IOU.",
                    tx.getInputs().isEmpty());
            require.using("There should be exactly one output state of type IOUState.",
                    tx.getOutputs().size() == 1);
            
            // ===== CONTENT CONSTRAINTS =====
            final IOUState output = tx.outputsOfType(IOUState.class).get(0);
            
            require.using("The IOU's value must be positive (greater than zero).",
                    output.getValue() > 0);
            require.using("The lender and the borrower cannot be the same entity.",
                    !output.getLender().equals(output.getBorrower()));
            
            // ===== SIGNER CONSTRAINTS =====
            require.using("Both lender and borrower must sign the IOU transaction.",
                    signers.containsAll(
                        output.getParticipants()
                              .stream()
                              .map(p -> p.getOwningKey())
                              .collect(Collectors.toList())
                    ));
            
            require.using("There must be exactly two signers.",
                    signers.size() == 2);
            
            return null;
        });
    }
    
    /**
     * Verification rules for Transfer command (for future use)
     */
    private void verifyTransfer(LedgerTransaction tx, List<PublicKey> signers) {
        requireThat(require -> {
            require.using("An IOU transfer must consume one input state.",
                    tx.getInputs().size() == 1);
            require.using("An IOU transfer must create one output state.",
                    tx.getOutputs().size() == 1);
            
            final IOUState input = tx.inputsOfType(IOUState.class).get(0);
            final IOUState output = tx.outputsOfType(IOUState.class).get(0);
            
            require.using("The IOU value cannot change during transfer.",
                    input.getValue() == output.getValue());
            require.using("The lender cannot change during transfer.",
                    input.getLender().equals(output.getLender()));
            
            return null;
        });
    }
    
    /**
     * Verification rules for Settle command (for future use)
     */
    private void verifySettle(LedgerTransaction tx, List<PublicKey> signers) {
        requireThat(require -> {
            require.using("An IOU settlement must consume one input state.",
                    tx.getInputs().size() == 1);
            require.using("An IOU settlement must not create output states.",
                    tx.getOutputs().isEmpty());
            
            return null;
        });
    }
    
    /**
     * Commands interface
     */
    public interface Commands extends CommandData {
        class Create implements Commands {}
        class Transfer implements Commands {}
        class Settle implements Commands {}
    }
}

Key Validation Rules:

  1. No Inputs: New IOUs start fresh (no consumed states)
  2. One Output: Exactly one IOU created
  3. Positive Value: Amount must be > 0
  4. Different Parties: Lender ≠ Borrower
  5. Both Sign: Lender and borrower must both sign
  6. Exactly Two Signers: No additional parties

Step 2: Enhanced Initiator Flow

File: workflows/src/main/java/com/template/flows/Initiator.java

package com.template.flows;

import co.paralleluniverse.fibers.Suspendable;
import com.google.common.collect.ImmutableList;
import com.template.contracts.IOUContract;
import com.template.states.IOUState;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.flows.*;
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.utilities.ProgressTracker.Step;

import java.util.Arrays;

@InitiatingFlow
@StartableByRPC
public class Initiator extends FlowLogic<SignedTransaction> {
    
    private final int iouValue;
    private final Party otherParty;
    
    // Progress tracking steps
    private static final Step GENERATING_TRANSACTION = new Step("Generating transaction.");
    private static final Step VERIFYING_TRANSACTION = new Step("Verifying contract constraints.");
    private static final Step SIGNING_TRANSACTION = new Step("Signing transaction with our private key.");
    private static final Step GATHERING_SIGS = new Step("Gathering the counterparty's signature.") {
        @Override
        public ProgressTracker childProgressTracker() {
            return CollectSignaturesFlow.Companion.tracker();
        }
    };
    private static final Step FINALISING_TRANSACTION = new Step("Obtaining notary signature and recording transaction.") {
        @Override
        public ProgressTracker childProgressTracker() {
            return FinalityFlow.Companion.tracker();
        }
    };
    
    private final ProgressTracker progressTracker = new ProgressTracker(
            GENERATING_TRANSACTION,
            VERIFYING_TRANSACTION,
            SIGNING_TRANSACTION,
            GATHERING_SIGS,
            FINALISING_TRANSACTION
    );
    
    public Initiator(int iouValue, Party otherParty) {
        this.iouValue = iouValue;
        this.otherParty = otherParty;
    }
    
    @Override
    public ProgressTracker getProgressTracker() {
        return progressTracker;
    }
    
    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        
        // Obtain a reference to the notary
        final Party notary = getServiceHub()
                .getNetworkMapCache()
                .getNotaryIdentities()
                .get(0);
        
        // Stage 1: Generate transaction
        progressTracker.setCurrentStep(GENERATING_TRANSACTION);
        
        // Create the output state
        final IOUState outputState = new IOUState(
            iouValue,
            getOurIdentity(),
            otherParty,
            new UniqueIdentifier()
        );
        
        // Build the transaction
        final TransactionBuilder txBuilder = new TransactionBuilder(notary)
                .addOutputState(outputState, IOUContract.ID)
                .addCommand(
                    new IOUContract.Commands.Create(),
                    Arrays.asList(
                        getOurIdentity().getOwningKey(),
                        otherParty.getOwningKey()
                    )
                );
        
        // Stage 2: Verify transaction
        progressTracker.setCurrentStep(VERIFYING_TRANSACTION);
        txBuilder.verify(getServiceHub());
        
        // Stage 3: Sign transaction
        progressTracker.setCurrentStep(SIGNING_TRANSACTION);
        final SignedTransaction partSignedTx = getServiceHub()
                .signInitialTransaction(txBuilder);
        
        // Stage 4: Collect counterparty signature
        progressTracker.setCurrentStep(GATHERING_SIGS);
        final FlowSession otherPartySession = initiateFlow(otherParty);
        final SignedTransaction fullySignedTx = subFlow(
                new CollectSignaturesFlow(
                        partSignedTx,
                        ImmutableList.of(otherPartySession),
                        GATHERING_SIGS.childProgressTracker()
                )
        );
        
        // Stage 5: Finalize transaction
        progressTracker.setCurrentStep(FINALISING_TRANSACTION);
        return subFlow(
                new FinalityFlow(
                        fullySignedTx,
                        ImmutableList.of(otherPartySession),
                        FINALISING_TRANSACTION.childProgressTracker()
                )
        );
    }
}

Enhancements:

  • Progress Tracking: Visual feedback on flow stages
  • Explicit Verification: Calls verify() before signing
  • Better Error Handling: Clear step-by-step execution
  • Immutable Collections: Uses ImmutableList for safety

Step 3: Custom Responder with Validation

File: workflows/src/main/java/com/template/flows/Responder.java

package com.template.flows;

import co.paralleluniverse.fibers.Suspendable;
import com.template.states.IOUState;
import net.corda.core.contracts.ContractState;
import net.corda.core.crypto.SecureHash;
import net.corda.core.flows.*;
import net.corda.core.transactions.SignedTransaction;

import static net.corda.core.contracts.ContractsDSL.requireThat;

@InitiatedBy(Initiator.class)
public class Responder extends FlowLogic<SignedTransaction> {
    
    private final FlowSession otherPartySession;
    
    public Responder(FlowSession otherPartySession) {
        this.otherPartySession = otherPartySession;
    }
    
    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        
        // Custom signing class with validation
        class SignTxFlow extends SignTransactionFlow {
            
            private SignTxFlow(FlowSession otherPartySession) {
                super(otherPartySession);
            }
            
            /**
             * Custom validation before signing.
             * The borrower can add their own business logic here.
             */
            @Override
            protected void checkTransaction(SignedTransaction stx) throws FlowException {
                requireThat(require -> {
                    // Extract the output state
                    ContractState output = stx.getTx().getOutputs().get(0).getData();
                    
                    // Check it's an IOU
                    require.using("This must be an IOU transaction.",
                            output instanceof IOUState);
                    
                    IOUState iou = (IOUState) output;
                    
                    // Example: Borrower only accepts IOUs above a minimum value
                    require.using("I will only accept IOU values of 100 or more.",
                            iou.getValue() >= 100);
                    
                    // Example: Borrower verifies they are indeed the borrower
                    require.using("I must be the borrower.",
                            iou.getBorrower().equals(getOurIdentity()));
                    
                    // Could add more custom checks:
                    // - Maximum IOU value
                    // - Whitelist of acceptable lenders
                    // - Time-based restrictions
                    // - Credit limit checks
                    
                    return null;
                });
            }
        }
        
        // Execute the signing flow
        final SignTxFlow signTxFlow = new SignTxFlow(otherPartySession);
        final SecureHash txId = subFlow(signTxFlow).getId();
        
        // Receive the finalized transaction
        return subFlow(new ReceiveFinalityFlow(otherPartySession, txId));
    }
}

Custom Validation Examples:

  • Minimum Value: Only accept IOUs ≥ 100
  • Identity Check: Verify correct borrower
  • Business Rules: Credit limits, whitelists, time windows

Step 4: Testing Scenarios

Test 1: Valid Transaction (Success)

# In PartyA terminal
flow start Initiator iouValue: 120, otherParty: "O=PartyB,L=New York,C=US"

# Output:
# ✓ Generating transaction
# ✓ Verifying contract constraints
# ✓ Signing transaction
# ✓ Gathering counterparty signature
# ✓ Finalising transaction
# Transaction committed: [TX_HASH]

# Verify in both vaults
run vaultQuery contractStateType: com.template.states.IOUState
# Shows: IOUState(value=120, lender=PartyA, borrower=PartyB)

Test 2: Negative Value (Contract Failure)

# In PartyA terminal
flow start Initiator iouValue: -5, otherParty: "O=PartyB,L=New York,C=US"

# Output:
# ✓ Generating transaction
# ✗ Verifying contract constraints
# 
# ERROR: Contract verification failed
# Reason: The IOU's value must be positive (greater than zero).
# 
# Flow failed

Test 3: Same Party (Contract Failure)

# Try to create IOU to yourself
flow start Initiator iouValue: 100, otherParty: "O=PartyA,L=London,C=GB"

# Output:
# ERROR: Contract verification failed
# Reason: The lender and the borrower cannot be the same entity.

Test 4: Value Below Threshold (Responder Rejection)

# In PartyA terminal
flow start Initiator iouValue: 90, otherParty: "O=PartyB,L=New York,C=US"

# Output:
# ✓ Generating transaction
# ✓ Verifying contract constraints
# ✓ Signing transaction
# ✓ Gathering counterparty signature
# ✗ Counterparty rejected transaction
# 
# ERROR: Flow failed at counterparty
# Reason: I will only accept IOU values of 100 or more.

Test 5: Query and Verify Privacy

# PartyA vault
run vaultQuery contractStateType: com.template.states.IOUState
# Shows: All IOUs where PartyA is lender or borrower

# PartyB vault
run vaultQuery contractStateType: com.template.states.IOUState
# Shows: All IOUs where PartyB is lender or borrower

# PartyC vault (if exists)
run vaultQuery contractStateType: com.template.states.IOUState
# Shows: Empty (no IOUs involving PartyC)

Complete Error Handling Examples

Common Errors & Fixes:

1. "No matching flow found"

# Wrong
flow start iouflow iouValue: 100, otherParty: "PartyB"

# Correct
flow start Initiator iouValue: 100, otherParty: "O=PartyB,L=New York,C=US"

Fix: Use exact class name and full X.500 party name

2. "Flow hangs indefinitely"

// Wrong - missing @Suspendable
public Void call() throws FlowException {
    // flow logic
}

// Correct
@Suspendable
public Void call() throws FlowException {
    // flow logic
}

Fix: Add @Suspendable annotation to flow methods

3. "Constraint Failed: Value must be positive"

# Creating IOU with 0 or negative value
flow start Initiator iouValue: 0, otherParty: "O=PartyB,L=New York,C=US"

Fix: Contract correctly prevents invalid data from entering ledger

4. "Transaction verification failed"

// Missing command signers
builder.addCommand(new IOUContract.Commands.Create(), Arrays.asList());

// Correct
builder.addCommand(
    new IOUContract.Commands.Create(),
    Arrays.asList(lender.getOwningKey(), borrower.getOwningKey())
);

Fix: Include all required signers in command

Practice Exercises

Beginner:

  1. Modify minimum IOU value in responder to 50 instead of 100
  2. Add a description field to IOUState and update contract
  3. Create 3 different IOUs with varying values and verify storage

Intermediate: 4. Implement a maximum IOU value constraint (e.g., ≤ 10,000) 5. Add oracle integration for currency conversion rates 6. Create a whitelist of acceptable lenders in the responder

Advanced: 7. Implement full Transfer flow to move IOU ownership 8. Add Settle flow to mark IOU as paid and remove from ledger 9. Create composite constraints: time windows, multi-stage approvals 10. Build REST API endpoints for creating/querying IOUs

Common Pitfalls

Contract Pitfalls:

  • Mistake: Not checking signer requirements

    • Impact: Unauthorized parties could sign
    • Fix: Verify signers match participants
  • Mistake: Forgetting to extract output state correctly

    // Wrong
    IOUState output = tx.getOutputs().get(0);
    
    // Correct
    IOUState output = tx.outputsOfType(IOUState.class).get(0);
  • Mistake: Using || instead of && for multiple conditions

    • Impact: Weak validation
    • Fix: Use && for all conditions to be true

Flow Pitfalls:

  • Mistake: Not restarting nodes after code changes

    • Impact: Old code still running
    • Fix: Stop nodes, rebuild, redeploy, restart
  • Mistake: Forgetting to create session before collecting signatures

    // Wrong
    subFlow(new CollectSignaturesFlow(partSignedTx, null));
    
    // Correct
    FlowSession session = initiateFlow(otherParty);
    subFlow(new CollectSignaturesFlow(partSignedTx, Arrays.asList(session)));
  • Mistake: Not handling FlowException properly

    • Impact: Unclear error messages
    • Fix: Add try-catch with specific error handling

Testing Pitfalls:

  • Mistake: Not testing negative scenarios

    • Impact: Missing validation bugs
    • Fix: Test all constraint violations
  • Mistake: Assuming all parties see all states

    • Impact: Privacy misconceptions
    • Fix: Always verify privacy by checking uninvolved party vaults

Final Project: Complete IOU CorDapp with Extensions

Project Goals

Build a production-ready IOU management system with:

  1. Create, transfer, and settle IOUs
  2. Multi-node network with observer
  3. Privacy verification
  4. Oracle integration for exchange rates
  5. REST API for external access

Implementation Phases

Phase 1: Core Functionality (Modules 5-6)

  • ✓ IOUState with lender, borrower, value
  • ✓ IOUContract with full validation
  • ✓ Initiator flow for creation
  • ✓ Responder flow with custom checks

Phase 2: Transfer Feature

// TransferFlow: Move IOU to new borrower
@InitiatingFlow
@StartableByRPC
public class TransferFlow extends FlowLogic<SignedTransaction> {
    private final UniqueIdentifier linearId;
    private final Party newBorrower;
    
    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        // Query existing IOU
        // Build transfer transaction
        // Collect signatures from old borrower, new borrower, lender
        // Finalize
    }
}

Phase 3: Settlement Feature

// SettleFlow: Mark IOU as paid
@InitiatingFlow
@StartableByRPC
public class SettleFlow extends FlowLogic<SignedTransaction> {
    private final UniqueIdentifier linearId;
    
    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        // Consume IOU state
        // No output state (IOU settled)
        // Both parties sign
        // Archive state
    }
}

Phase 4: 4-Node Network

// Add observer node
node {
    name "O=Observer,L=Paris,C=FR"
    p2pPort 10011
    rpcSettings {
        address("localhost:10012")
        adminAddress("localhost:10052")
    }
}

Test Privacy:

# Create IOU between PartyA and PartyB
# Verify Observer node has empty vault
run vaultQuery contractStateType: com.template.states.IOUState
# Result: Empty

Phase 5: Oracle Integration

// ExchangeRateOracle: Provide currency conversion
@InitiatingFlow
@StartableByRPC
public class QueryExchangeRateFlow extends FlowLogic<Double> {
    private final String fromCurrency;
    private final String toCurrency;
    
    @Suspendable
    @Override
    public Double call() throws FlowException {
        // Query external API
        // Sign rate data
        // Return to caller
    }
}

// Use in IOU creation for multi-currency support

Phase 6: REST API

// Spring Boot controller
@RestController
@RequestMapping("/api/iou")
public class IOUController {
    
    @PostMapping("/create")
    public ResponseEntity<String> createIOU(
        @RequestBody IOURequest request
    ) {
        // Start Initiator flow via RPC
        // Return transaction ID
    }
    
    @GetMapping("/list")
    public ResponseEntity<List<IOUState>> listIOUs() {
        // Query vault via RPC
        // Return all IOUs
    }
}

Testing Checklist

Functional Tests:

  • Create IOU with valid data
  • Reject negative values
  • Reject same lender/borrower
  • Reject insufficient signers
  • Transfer IOU to new party
  • Settle IOU completely
  • Query by linear ID
  • Query by party

Privacy Tests:

  • Verify only participants see state
  • Confirm observer sees nothing
  • Check vault isolation

Performance Tests:

  • Create 100 IOUs sequentially
  • Create 10 IOUs in parallel
  • Measure finality time

Integration Tests:

  • REST API create endpoint
  • REST API query endpoint
  • Oracle rate lookup
  • Multi-currency conversion

Deployment Checklist

Development:

  • Code compiles without errors
  • All unit tests pass
  • Contract verification passes
  • Flows execute successfully

Staging:

  • Deploy to test network (3+ nodes)
  • Test all flows end-to-end
  • Verify logging and monitoring
  • Load test with realistic data

Production:

  • Security audit completed
  • Notary configured and tested
  • Backup and recovery plan
  • Documentation complete
  • Training materials ready

Additional Resources

Official Documentation

Code Repositories

Community & Support

Learning Path Progression

Week 1-2: Foundations

  • Complete Modules 1-3 (Theory)
  • Set up development environment
  • Run sample CorDapps

Week 3-4: Implementation

  • Complete Modules 4-6 (Coding)
  • Build basic IOU app
  • Test all scenarios

Week 5-6: Advanced Features

  • Implement transfer and settlement
  • Add oracle integration
  • Build REST API

Week 7-8: Production Ready

  • Complete final project
  • Deploy multi-node network
  • Documentation and testing

Conclusion

This unified guide provides a comprehensive learning path for Corda development, combining:

  • Theoretical foundations (Modules 1-3)
  • Practical setup (Module 4)
  • Hands-on coding (Modules 5-6)
  • Real-world extensions (Final Project)

Key Takeaways:

  1. Corda is enterprise-focused DLT, not public blockchain
  2. Privacy is paramount: need-to-know data sharing
  3. States are immutable: consumed and recreated
  4. Contracts enforce rules: validation before ledger entry
  5. Flows automate processes: multi-party coordination
  6. Notaries prevent double-spending: uniqueness consensus

Success Criteria:

  • ✓ Understand Corda architecture and philosophy
  • ✓ Set up and run development environment
  • ✓ Build complete CorDapp from scratch
  • ✓ Implement contracts with validation
  • ✓ Create flows for multi-party workflows
  • ✓ Test and verify privacy features
  • ✓ Deploy multi-node networks

Next Steps:

  1. Complete all practice exercises
  2. Extend IOU app with custom features
  3. Explore advanced patterns (oracles, time windows)
  4. Join Corda community
  5. Build your own production use case

Remember: Corda development is iterative. Start simple, test thoroughly, and gradually add complexity. The key to mastery is building real applications and learning from the experience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment