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
Video: What is Corda? | Enterprise Blockchain
Duration: ~10 minutes
- 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
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 |
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
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
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
A specialized service (or node) that:
- Prevents "double-spending"
- Provides uniqueness consensus
- Stamps transactions for immediate finality
- Doesn't see transaction data (privacy preserved)
- Developed by R3 (founded 2014 as bank consortium)
- Open-source core with enterprise edition available
- Smart contracts written in Kotlin/Java (CorDapps)
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
- Compare Corda to Ethereum for a banking settlement use case: List 3 pros and 3 cons for each
- Sketch a transaction diagram for a private loan between two financial institutions
- Explain why a hospital network would choose Corda over Bitcoin for patient records
- 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
Video: More about Corda | R3
Duration: ~8 minutes
- 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
- 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
- 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
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 (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
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 |
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
- Diagram a 3-node network for secure document sharing between law firms
- Explain why Corda avoids miners and how this benefits enterprises
- Compare the cost structure of Ethereum vs. Corda for 1000 daily transactions
- 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
Video: Corda Key Concepts
Duration: ~12 minutes
- 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
| 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 |
- 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"
- Point-to-point connections between nodes
- Network Map Service provides:
- Node discovery (name → network address)
- Notary listing
- Network parameters
- Customizable topology per business needs
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
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);
}
}Atomic operation that:
- Consumes input states (marks them as historical)
- Creates output states (new current facts)
- Executes commands (intent)
- 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
Legal agreement encoded in code:
Key Responsibilities:
- Verify transaction structure is valid
- Check business logic constraints
- 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
}
}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())
);Critical Role: Prevents double-spending
How It Works:
- Transaction submitted to notary
- Notary checks if input states are unspent
- If valid: Signs and timestamps
- If already spent: Rejects with conflict details
Types:
- Validating Notary: Sees transaction details, validates contracts
- Non-Validating Notary: Only checks uniqueness (privacy preserved)
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:
- Build transaction
- Verify locally
- Sign transaction
- Collect counterparty signatures
- Notarize
- 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
-
Validity Consensus:
- All participants verify contract rules
- All required parties sign
- Happens before notarization
-
Uniqueness Consensus:
- Notary ensures states haven't been double-spent
- Single authority for finality
- Happens at notarization
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
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 signed3. 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
- Define a state for a medical record transfer including patient consent
- Outline a complete flow for an interbank payment with compliance checks
- Design a contract for a supply chain shipment with quality verification
- Explain how UTXO model prevents double-spending without seeing all transactions
- 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)
Video: Exploring Cordapp
Duration: ~15 minutes
- 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
JVM-based applications running on Corda network:
- Written in Java or Kotlin
- Deployed to nodes as JAR files
- Contains contracts (rules) and workflows (flows)
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 = "I Owe You" debt token
- Lender: Party who lent money
- Borrower: Party who owes money
- Value: Amount owed
- Linear state that tracks debt obligations
- @InitiatingFlow: Lender starts IOU creation
- @InitiatedBy: Borrower receives and accepts
- @StartableByRPC: Callable via command line or API
Interface for interacting with nodes:
- Command-line shell (CRaSH)
- REST API
- Custom clients
1. Install Prerequisites:
# Verify Java 8
java -version
# Should show: java version "1.8.x"
# Verify Gradle (via wrapper)
./gradlew --version
# Verify Git
git --version2. 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.git3. 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")
}
}
}1. Build the Project:
# Windows
gradlew.bat clean build
# Mac/Linux
./gradlew clean build2. 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"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
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:
- Shape: No inputs, exactly one output
- Value: Must be positive
- Parties: Lender ≠ Borrower
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));
}
}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": "..."
# }
# ]- Add PartyC as an observer; verify it cannot see IOUs between A and B
- Modify curl request to send IOU value of 0; observe the contract failure
- Create 5 IOUs with different values; query and compare vaults
- Try to create IOU where lender = borrower; verify the contract prevents it
- Implement a REST endpoint to query IOUs by minimum value
- 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:99notiouValue: 99 - Node Startup: Wait for "Welcome to Corda" message before running commands
- Gradle Sync: Always let IntelliJ complete Gradle sync before building
Video: Writing a Cordapp | Part 1
Duration: ~10 minutes
- 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
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)
Core class for assembling transactions:
TransactionBuilder builder = new TransactionBuilder(notary);
builder.addOutputState(state, contractID);
builder.addCommand(command, signers);
builder.verify(serviceHub); // Pre-check before signingNode's interface to Corda services:
getNetworkMapCache(): Find parties and notariessignInitialTransaction(): Sign with node's keygetVaultService(): Query stored states
Required for flow methods that pause/resume:
@Suspendable
@Override
public Void call() throws FlowException {
// Flow logic that waits for network responses
}1. Get Template:
git clone https://github.com/corda/cordapp-template-java.git
cd cordapp-template-java2. 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
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)
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.
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:
- Find Notary: Get first available notary from network
- Create State: Build IOUState with data
- Build Transaction: Add outputs and commands
- Verify: Pre-check contract rules
- Sign: Lender signs first
- Collect Signatures: Get borrower's signature
- Finalize: Notarize and distribute to vaults
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.
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")
}
}
}1. Build:
# Windows
gradlew.bat clean build
# Mac/Linux
./gradlew clean build2. Deploy Nodes:
# Windows
gradlew.bat deployNodes
# Mac/Linux
./gradlew deployNodes
# Creates: build/nodes/ with PartyA, PartyB, Notary3. Start Network:
# Windows
build\nodes\runnodes.bat
# Mac/Linux
build/nodes/runnodes
# Wait for all nodes to show: "Welcome to Corda interactive shell"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)- Add a
descriptionfield to IOUState for note about the debt - Create a third command
Cancelin the contract - Implement logging in the Initiator flow using
logger.info() - Try creating IOU from PartyB to PartyA (reverse direction)
- Add a
Movecommand and basic transfer flow
- 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 buildbeforedeployNodes - Party Name Typos: X.500 names are case-sensitive and format-strict
- Old Nodes Running: Stop previous nodes before starting new ones
Video: Writing a Cordapp | Part 2
Duration: ~12 minutes
- 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
Corda's Domain-Specific Language for readable constraints:
requireThat(require -> {
require.using("Error message", condition);
require.using("Another message", another_condition);
return null;
});Built-in flow for gathering signatures from multiple parties:
SignedTransaction fullySignedTx = subFlow(
new CollectSignaturesFlow(
partiallySignedTx,
sessions,
CollectSignaturesFlow.tracker()
)
);Base class for custom responder validation:
class CustomSignTxFlow extends SignTransactionFlow {
@Override
protected void checkTransaction(SignedTransaction stx) {
// Custom validation before signing
}
}Multiple layers of validation:
- Shape Constraints: Number of inputs/outputs
- Content Constraints: Data validity
- Signer Constraints: Required signatures
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:
- No Inputs: New IOUs start fresh (no consumed states)
- One Output: Exactly one IOU created
- Positive Value: Amount must be > 0
- Different Parties: Lender ≠ Borrower
- Both Sign: Lender and borrower must both sign
- Exactly Two Signers: No additional parties
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
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
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 failedTest 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)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
Beginner:
- Modify minimum IOU value in responder to 50 instead of 100
- Add a
descriptionfield to IOUState and update contract - 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
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
Build a production-ready IOU management system with:
- Create, transfer, and settle IOUs
- Multi-node network with observer
- Privacy verification
- Oracle integration for exchange rates
- REST API for external access
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: EmptyPhase 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 supportPhase 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
}
}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
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
- Corda Docs: https://docs.corda.net
- API Reference: https://api.corda.net
- Training: https://training.corda.net
- Samples: https://github.com/corda/samples
- Templates: https://github.com/corda/cordapp-template-java
- Community: https://github.com/corda-community
- Slack: https://slack.corda.net
- Stack Overflow: Tag
corda - Forum: https://discourse.corda.net
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
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:
- Corda is enterprise-focused DLT, not public blockchain
- Privacy is paramount: need-to-know data sharing
- States are immutable: consumed and recreated
- Contracts enforce rules: validation before ledger entry
- Flows automate processes: multi-party coordination
- 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:
- Complete all practice exercises
- Extend IOU app with custom features
- Explore advanced patterns (oracles, time windows)
- Join Corda community
- 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.