Episode 6 — Scaling Reliability Microservices Web3 / 6.10 — Web3 Basics
6.10.c — Decentralized Applications (DApps)
In one sentence: A DApp (Decentralized Application) is an application where the backend logic runs on a blockchain via smart contracts instead of centralized servers, enabling censorship-resistant, transparent, and user-owned digital experiences.
Navigation: <- 6.10.b — Blockchain Fundamentals | 6.10.d — Smart Contracts ->
1. What Are DApps?
A Decentralized Application (DApp) is an application that runs on a decentralized network rather than a single server. The key difference from traditional applications: the backend logic and state live on a blockchain, not on servers you control.
What makes an app a DApp:
1. OPEN SOURCE — Code is publicly verifiable
2. DECENTRALIZED — Runs on a blockchain (no single point of failure)
3. TOKEN-BASED — Uses crypto tokens for access, governance, or value
4. CONSENSUS — Changes require network agreement
What does NOT make an app a DApp:
- Having a crypto payment option (that's just a feature)
- Using blockchain for logging (that's just an audit trail)
- Having a token that does nothing (that's marketing)
2. Architecture Comparison: Traditional App vs DApp
This is the most important mental model shift for backend engineers.
Traditional Application Architecture
┌──────────────────────────────────────────────────────────┐
│ TRADITIONAL WEB APPLICATION │
│ │
│ ┌──────────┐ HTTP/REST ┌──────────────┐ │
│ │ Frontend │ ──────────────>│ Backend │ │
│ │ (React) │ <──────────────│ (Express) │ │
│ └──────────┘ JSON └──────┬───────┘ │
│ │ │
│ │ SQL/ORM │
│ ▼ │
│ ┌──────────────┐ │
│ │ Database │ │
│ │ (PostgreSQL) │ │
│ └──────────────┘ │
│ │
│ Controlled by: ONE company │
│ Data stored: Company's servers │
│ Authentication: Username + password │
│ Payments: Stripe / PayPal │
└──────────────────────────────────────────────────────────┘
DApp Architecture
┌──────────────────────────────────────────────────────────┐
│ DECENTRALIZED APPLICATION (DApp) │
│ │
│ ┌──────────┐ ethers.js ┌──────────────┐ │
│ │ Frontend │ ──────────────>│ Blockchain │ │
│ │ (React) │ <──────────────│ (Ethereum) │ │
│ └────┬─────┘ JSON-RPC └──────┬───────┘ │
│ │ │ │
│ │ │ EVM executes │
│ │ ▼ │
│ │ ┌──────────────┐ │
│ │ │Smart Contract │ │
│ │ │ (Solidity) │ │
│ │ └──────────────┘ │
│ │ │
│ │ ┌────────────┐ ┌──────────────┐ │
│ └─>│ Wallet │ │ IPFS │ │
│ │ (MetaMask) │ │ (file storage)│ │
│ └────────────┘ └──────────────┘ │
│ │
│ Controlled by: NO single entity │
│ Data stored: Blockchain + IPFS (distributed) │
│ Authentication: Wallet signature │
│ Payments: Native crypto │
└──────────────────────────────────────────────────────────┘
Side-by-Side Comparison
| Aspect | Traditional App | DApp |
|---|---|---|
| Backend | Express/Django on servers | Smart contracts on blockchain |
| Database | PostgreSQL/MongoDB | Blockchain state |
| File storage | S3, local disk | IPFS, Arweave |
| Authentication | JWT, sessions, OAuth | Wallet signature (cryptographic) |
| Payments | Stripe, PayPal | Native crypto (ETH, tokens) |
| Hosting | AWS, Vercel, Heroku | Blockchain nodes (decentralized) |
| Frontend hosting | Vercel, Netlify | IPFS, Vercel (often still centralized) |
| Updates | Redeploy server | Deploy new contract (old one remains) |
| Downtime | Server outage = app down | Blockchain nodes always running |
| Data privacy | Private by default | Public by default (on-chain) |
| Cost model | Server costs (monthly) | Gas fees (per transaction) |
| Speed | Milliseconds | Seconds to minutes |
3. Wallet Connection (MetaMask)
In Web3, users authenticate with a wallet instead of a username/password. The wallet holds the user's private keys and signs transactions.
How Wallet Authentication Works
TRADITIONAL LOGIN: WEB3 WALLET LOGIN:
1. User types email/password 1. User clicks "Connect Wallet"
2. Server checks database 2. MetaMask pops up
3. Server creates JWT 3. User approves connection
4. Client stores JWT 4. DApp reads wallet address
5. JWT sent with each request 5. For actions: user SIGNS with private key
Trust: Server knows your password Trust: Only you have your private key
Risk: Server breach = data leak Risk: Phishing, user error
Connecting to MetaMask with ethers.js
import { ethers } from 'ethers';
// Check if MetaMask is installed
if (typeof window.ethereum === 'undefined') {
console.log('Please install MetaMask!');
} else {
// Request account access
const provider = new ethers.BrowserProvider(window.ethereum);
// This triggers the MetaMask popup asking user to connect
const signer = await provider.getSigner();
const address = await signer.getAddress();
console.log('Connected wallet:', address);
// Output: "Connected wallet: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
// Get the user's ETH balance
const balance = await provider.getBalance(address);
console.log('Balance:', ethers.formatEther(balance), 'ETH');
}
Signing Messages (Proof of Identity)
// The user signs a message to prove they own the wallet
// This does NOT cost gas — it's just a cryptographic signature
const message = 'Sign in to MyDApp at 2024-03-15T14:23:07Z';
const signature = await signer.signMessage(message);
console.log('Signature:', signature);
// Output: "0x3a4b5c..." (65-byte signature)
// Anyone can verify this signature was made by the wallet owner
const recoveredAddress = ethers.verifyMessage(message, signature);
console.log('Signer:', recoveredAddress);
// Output matches the connected wallet address
4. Web3.js and Ethers.js Libraries
Two JavaScript libraries dominate blockchain interaction. Ethers.js is the modern standard.
Ethers.js vs Web3.js
| Feature | ethers.js | Web3.js |
|---|---|---|
| Bundle size | ~120 KB (small) | ~590 KB (large) |
| Design | Modular, clean API | Monolithic |
| TypeScript | First-class support | Added later |
| Provider/Signer | Separated (good practice) | Combined |
| Popularity | Growing (recommended) | Legacy (still widely used) |
| Documentation | Excellent | Good |
| Maintained by | Richard Moore | ChainSafe |
Key Concepts in ethers.js
import { ethers } from 'ethers';
// ========================================
// PROVIDER — read-only connection to blockchain
// ========================================
// Think of it like a "database read connection"
// Connect to Ethereum mainnet via a node provider
const provider = new ethers.JsonRpcProvider(
'https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'
);
// Read-only operations (no wallet needed):
const blockNumber = await provider.getBlockNumber();
const balance = await provider.getBalance('0xAddress...');
const network = await provider.getNetwork();
// ========================================
// SIGNER — wallet that can sign transactions
// ========================================
// Think of it like a "database write connection"
// From a browser wallet (MetaMask)
const browserProvider = new ethers.BrowserProvider(window.ethereum);
const signer = await browserProvider.getSigner();
// From a private key (backend/scripts — NEVER expose private keys!)
const wallet = new ethers.Wallet('0xPRIVATE_KEY', provider);
// ========================================
// CONTRACT — interact with a smart contract
// ========================================
// Think of it like an "ORM model"
const contractAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // USDC
const abi = [
'function balanceOf(address owner) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'event Transfer(address indexed from, address indexed to, uint256 value)'
];
// Read-only contract instance
const readContract = new ethers.Contract(contractAddress, abi, provider);
const usdcBalance = await readContract.balanceOf('0xSomeAddress...');
// Read-write contract instance (needs a signer)
const writeContract = new ethers.Contract(contractAddress, abi, signer);
const tx = await writeContract.transfer('0xRecipient...', ethers.parseUnits('100', 6));
await tx.wait(); // Wait for transaction confirmation
5. Reading Blockchain Data
Reading data from the blockchain is free (no gas costs). It's the equivalent of a SELECT query.
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://eth-mainnet.g.alchemy.com/v2/KEY');
// ----- Read basic blockchain data -----
const block = await provider.getBlock('latest');
console.log('Block number:', block.number);
console.log('Timestamp:', new Date(block.timestamp * 1000));
console.log('Transactions:', block.transactions.length);
// ----- Read a specific transaction -----
const tx = await provider.getTransaction('0xTransactionHash...');
console.log('From:', tx.from);
console.log('To:', tx.to);
console.log('Value:', ethers.formatEther(tx.value), 'ETH');
// ----- Read transaction receipt (after confirmation) -----
const receipt = await provider.getTransactionReceipt('0xTransactionHash...');
console.log('Status:', receipt.status === 1 ? 'Success' : 'Failed');
console.log('Gas used:', receipt.gasUsed.toString());
// ----- Listen for new blocks (real-time) -----
provider.on('block', (blockNumber) => {
console.log('New block:', blockNumber);
});
// ----- Listen for contract events -----
const contract = new ethers.Contract(contractAddress, abi, provider);
contract.on('Transfer', (from, to, value) => {
console.log(`Transfer: ${from} -> ${to}: ${ethers.formatUnits(value, 6)} USDC`);
});
6. IPFS for Decentralized Storage
IPFS (InterPlanetary File System) is a peer-to-peer file storage protocol. Instead of storing files on a single server, IPFS distributes them across a network of nodes.
TRADITIONAL FILE STORAGE: IPFS:
https://myserver.com/image.png ipfs://QmX3a8b...7c9d
│ │
▼ ▼
┌──────────┐ ┌───┐ ┌───┐ ┌───┐
│ One server│ │ N1│ │ N2│ │ N3│
│ (AWS S3) │ └───┘ └───┘ └───┘
└──────────┘ Any node with the file
can serve it
Server goes down = file gone Content-addressable:
URL can change The hash IS the address
Censorable Cannot be censored
Why DApps Use IPFS
Storing large files (images, videos, metadata) directly on the blockchain is prohibitively expensive (~$20,000 per MB on Ethereum). DApps store large data on IPFS and only store the IPFS hash on-chain.
NFT storage pattern:
On-chain (smart contract):
tokenURI = "ipfs://QmX3a8b...7c9d" ← only the hash (32 bytes, cheap)
On IPFS (decentralized storage):
{
"name": "Cool NFT #42",
"description": "A very cool digital artwork",
"image": "ipfs://QmY4b9c...8d0e", ← image stored on IPFS too
"attributes": [
{ "trait_type": "Color", "value": "Blue" },
{ "trait_type": "Rarity", "value": "Legendary" }
]
}
7. DApp Categories
| Category | What It Does | Examples |
|---|---|---|
| DeFi | Decentralized finance — lending, borrowing, trading | Uniswap, Aave, Compound, MakerDAO |
| NFT Marketplaces | Buy, sell, and trade NFTs | OpenSea, Blur, Magic Eden |
| Gaming | Play-to-earn, on-chain game assets | Axie Infinity, Gods Unchained |
| Social | Decentralized social media | Lens Protocol, Farcaster |
| Identity | Self-sovereign identity, domains | ENS (Ethereum Name Service) |
| DAOs | Community governance | Uniswap Governance, Gitcoin |
| Infrastructure | Developer tools, indexing | The Graph, Chainlink (oracles) |
| Storage | Decentralized file/data storage | IPFS, Filecoin, Arweave |
8. Advantages and Disadvantages of DApps
Advantages
| Advantage | Explanation |
|---|---|
| Censorship resistance | No single entity can shut down the app or block users |
| Transparency | All code and transactions are publicly verifiable |
| No downtime | Blockchain networks run 24/7 (no server maintenance windows) |
| User ownership | Users own their data and assets (tokens, NFTs) |
| Composability | DApps can integrate with other DApps seamlessly ("money legos") |
| Global access | Anyone with internet + wallet can participate |
| Trustless | Users don't need to trust the developers — the code is the law |
Disadvantages
| Disadvantage | Explanation |
|---|---|
| Poor UX | Wallets, gas fees, and confirmations are confusing for normal users |
| Slow | Blockchain confirmations take seconds to minutes (vs milliseconds for servers) |
| Expensive | Every write operation costs gas; costs spike during congestion |
| Immutable bugs | Deployed smart contract bugs cannot be easily patched |
| Limited storage | On-chain storage is extremely expensive |
| Scalability | Current blockchains handle far fewer transactions than centralized systems |
| Irreversible | Mistakes (wrong address, lost keys) cannot be undone |
| Regulatory risk | Uncertain legal frameworks across jurisdictions |
| Key management | Losing your private key means losing everything — no "forgot password" |
9. Current DApp Ecosystem
DApp Ecosystem by TVL (Total Value Locked) — approximate figures:
DeFi Protocols:
Lido (liquid staking): ~$30B TVL
Aave (lending/borrowing): ~$15B TVL
MakerDAO (stablecoins): ~$8B TVL
Uniswap (decentralized exchange): ~$6B TVL
Eigenlayer (restaking): ~$10B TVL
By Blockchain:
Ethereum: ~60% of all DeFi TVL
Solana: ~8%
BSC: ~5%
Arbitrum: ~5%
Polygon: ~3%
Others: ~19%
Active DApp users (daily unique wallets):
~5-10 million daily active wallets across all chains
Compare to: Facebook ~2 billion daily active users
10. Simple DApp Interaction Example
Here is a complete example of interacting with a DApp (Uniswap-style token swap) from a frontend:
import { ethers } from 'ethers';
// ========================================
// Step 1: Connect wallet
// ========================================
async function connectWallet() {
if (!window.ethereum) {
throw new Error('No wallet found. Install MetaMask.');
}
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
const balance = await provider.getBalance(address);
return {
provider,
signer,
address,
balance: ethers.formatEther(balance)
};
}
// ========================================
// Step 2: Read data from a smart contract
// ========================================
async function getTokenBalance(provider, tokenAddress, walletAddress) {
const abi = [
'function balanceOf(address) view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)'
];
const contract = new ethers.Contract(tokenAddress, abi, provider);
const [balance, decimals, symbol] = await Promise.all([
contract.balanceOf(walletAddress),
contract.decimals(),
contract.symbol()
]);
return {
raw: balance,
formatted: ethers.formatUnits(balance, decimals),
symbol,
decimals
};
}
// ========================================
// Step 3: Send a transaction (costs gas!)
// ========================================
async function sendToken(signer, tokenAddress, recipientAddress, amount) {
const abi = [
'function transfer(address to, uint256 amount) returns (bool)',
'function decimals() view returns (uint8)'
];
const contract = new ethers.Contract(tokenAddress, abi, signer);
const decimals = await contract.decimals();
const amountInWei = ethers.parseUnits(amount.toString(), decimals);
console.log(`Sending ${amount} tokens to ${recipientAddress}...`);
// This triggers MetaMask to ask for user approval
const tx = await contract.transfer(recipientAddress, amountInWei);
console.log('Transaction hash:', tx.hash);
// Wait for confirmation
const receipt = await tx.wait();
console.log('Confirmed in block:', receipt.blockNumber);
console.log('Gas used:', receipt.gasUsed.toString());
return receipt;
}
// ========================================
// Step 4: Listen for events
// ========================================
async function listenForTransfers(provider, tokenAddress) {
const abi = [
'event Transfer(address indexed from, address indexed to, uint256 value)'
];
const contract = new ethers.Contract(tokenAddress, abi, provider);
contract.on('Transfer', (from, to, value, event) => {
console.log(`Transfer detected!`);
console.log(` From: ${from}`);
console.log(` To: ${to}`);
console.log(` Value: ${value.toString()}`);
console.log(` Block: ${event.log.blockNumber}`);
});
console.log('Listening for transfer events...');
}
// ========================================
// Usage
// ========================================
async function main() {
const { provider, signer, address, balance } = await connectWallet();
console.log(`Connected: ${address} (${balance} ETH)`);
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
// Read USDC balance
const usdc = await getTokenBalance(provider, USDC_ADDRESS, address);
console.log(`USDC Balance: ${usdc.formatted} ${usdc.symbol}`);
// Send 10 USDC (user approves in MetaMask)
// await sendToken(signer, USDC_ADDRESS, '0xRecipient...', 10);
// Listen for all USDC transfers
await listenForTransfers(provider, USDC_ADDRESS);
}
main().catch(console.error);
11. Key Takeaways
- DApps replace centralized backends with smart contracts — the blockchain is both your server and database, with the trade-off of speed and cost for decentralization and censorship resistance.
- Architecture shift — traditional apps use REST APIs and databases; DApps use ethers.js, smart contracts, and blockchain state. Frontends remain similar (React).
- Wallet = identity — MetaMask and similar wallets replace username/password authentication with cryptographic signatures.
- ethers.js is the standard library — use Providers for reading, Signers for writing, and Contracts for interacting with smart contracts.
- Reading is free, writing costs gas — every state-changing transaction costs gas, which varies by complexity and network congestion.
- IPFS solves the storage problem — large files go on IPFS, only the content hash is stored on-chain.
- Real trade-offs exist — DApps are slower, more expensive, and harder to use than traditional apps, but they offer censorship resistance, transparency, and user ownership.
Explain-It Challenge
- Your product manager asks "why can't we just use a normal database instead of a blockchain for our DApp?" Give three scenarios where a blockchain genuinely adds value and three where it does not.
- A user reports that their MetaMask transaction has been "pending" for 30 minutes. Explain what happened and how to fix it.
- Design the architecture for a decentralized marketplace (like OpenSea). Which parts live on-chain, which on IPFS, and which might still need centralized infrastructure?
Navigation: <- 6.10.b — Blockchain Fundamentals | 6.10.d — Smart Contracts ->