Designing dApp Architecture
dApp architecture is not just "React frontend + smart contract." It's a thoughtful system where frontend, indexer, wallet integration, and contracts work as one unit with good UX. A typical mistake: developing frontend separately from contract architecture — later it turns out the UI can't efficiently read needed data.
Key dApp Components
Smart Contract Layer
Contracts are designed with frontend-ability in mind:
- Rich events for indexing (all important actions are logged)
- View functions for reading without gas
- Multicall compatibility
State Management
// Modern dApp stack: wagmi v2 + viem + TanStack Query
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { formatUnits } from "viem";
function PoolStats() {
const { data: tvl } = useReadContract({
address: POOL_ADDRESS,
abi: POOL_ABI,
functionName: "totalAssets",
query: { refetchInterval: 30_000 }, // update every 30 seconds
});
const { data: userBalance } = useReadContract({
address: POOL_ADDRESS,
abi: POOL_ABI,
functionName: "balanceOf",
args: [address],
});
return (
<div>
<p>TVL: {tvl ? formatUnits(tvl, 18) : "Loading..."} ETH</p>
<p>Your balance: {userBalance ? formatUnits(userBalance, 18) : "0"}</p>
</div>
);
}
Batch RPC Calls (Multicall3)
Avoid N separate RPC requests through batching:
import { multicall } from "viem/actions";
// Instead of 10 separate requests — one
const results = await multicall(client, {
contracts: [
{ address: TOKEN_A, abi: ERC20_ABI, functionName: "balanceOf", args: [user] },
{ address: TOKEN_B, abi: ERC20_ABI, functionName: "balanceOf", args: [user] },
{ address: POOL, abi: POOL_ABI, functionName: "totalAssets" },
// ...7 more requests
],
});
Transaction Flow UX
function DepositButton({ amount }: { amount: bigint }) {
const { data: hash, writeContract, isPending } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
// States: idle → pending (wallet) → confirming (chain) → success/error
const status = isPending ? "WAITING_WALLET"
: isConfirming ? "CONFIRMING"
: isSuccess ? "SUCCESS"
: "IDLE";
return (
<button
onClick={() => writeContract({ address: POOL, abi, functionName: "deposit", args: [amount] })}
disabled={status !== "IDLE"}
>
{status === "WAITING_WALLET" && "Confirm in wallet..."}
{status === "CONFIRMING" && "Confirming..."}
{status === "SUCCESS" && "Deposited!"}
{status === "IDLE" && "Deposit"}
</button>
);
}
Data Fetching Architecture
Realtime (< 1 sec latency): WebSocket to RPC or Alchemy SDK
Recent history (last 1000 events): The Graph (subgraph)
Historical analytics: Self-hosted PostgreSQL indexer
Prices: CoinGecko API + Chainlink on-chain
Wallet Connection
// RainbowKit configuration
import { getDefaultConfig, RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { arbitrum, mainnet, base } from "wagmi/chains";
const config = getDefaultConfig({
appName: "MyDApp",
projectId: WALLETCONNECT_PROJECT_ID,
chains: [mainnet, arbitrum, base],
wallets: [
{ groupName: "Popular", wallets: [metaMaskWallet, coinbaseWallet, walletConnectWallet] },
],
});
Designing dApp architecture considering all layers: 1-2 weeks. Includes contract interaction wireframes, data flow diagrams, frontend stack selection, and UX flow documentation.







