Back to Blog
Web3Code RefactoringEngineering

Web3 Code Refactoring: When to Modernize a Legacy Codebase

May 20, 2026
8 min read

Most Web3 apps don't break on the chain. They break around it.

The on-chain code is usually fine. Audited contracts don't rot. What rots is the rest: the frontend, the wallet handling, the event listeners, the type definitions, the state management. That stuff gets bolted together during a bull market, when shipping fast is the only thing that matters, and nobody goes back to clean it up.

I see the same pattern again and again. The DeFi platform runs fine. Transactions confirm. The numbers hold. But every deploy takes a day of prep and three nervous developers. Adding a new chain takes a month. A small UI change touches seven files. The blockchain part is fine. It's everything around it.

So how do you know the mess is costing you more than the fix would? Here's what to look for.

The Signs That Say "Refactor Now"

Every feature takes 3x longer than it should

This is the clearest one. You estimate two days. It takes six, because of side effects nobody saw coming. That's not a people problem. It's a structure problem. In Web3 it usually means wallet connection logic spread across components, or ABI types (the description of a contract's functions) copied into ten different places.

You can't add a chain without touching thirty files

A well-built Web3 app hides the chain behind a layer. The component that reads a token balance shouldn't care if the balance comes from Ethereum, Polygon, or Arbitrum. If yours does care, if you have if (chainId === 137) scattered through the UI, you have a coupling problem. Adding a network turns into find-and-replace across the whole codebase.

Your wallet connection code is older than the libraries it uses

The wallet stack changed a lot between 2021 and 2024. Web3Modal became WalletConnect v2. wagmi went from v0 to v2 with breaking changes at each step. ethers.js v5 and v6 do almost everything differently. If your app was written when these were young, you're carrying dead patterns, removed methods, and bugs that later versions already patched.

You don't have typed ABIs

If your contract calls look like contract.functions.transfer(to, amount) with no TypeScript checking on the arguments or the return value, you're flying blind. A typed ABI (generated with wagmi CLI or typechain) checks every call at compile time. Without it, you collect silent bugs: wrong argument order, wrong type, wrong units. They only show up at runtime, in front of a user.

Onboarding a new developer takes more than a week just to understand the architecture

This is the tax bad architecture charges on every hire. If a senior developer can't get productive in three to five days, you're paying that cost again and again, with every new person on the team.

What to Fix First

You don't have to fix everything at once. The goal isn't a perfect codebase. The goal is to remove the friction that's actively costing you time and safety. Do it in this order.

1. Wallet and provider abstraction

Pull all wallet connection logic into one module or hook. This single move usually cuts your window.ethereum references and useAccount hooks from thirty places down to one. Every component that needs a signer or an address goes through this one door. When WalletConnect v3 ships, you change one file.

The simple version is a useWallet hook that returns { address, signer, chainId, connect, disconnect }. Every component that needs wallet state uses this, and nothing else.

2. Generate typed ABIs

Use wagmi CLI (bunx wagmi generate) or typechain to build TypeScript types from your contract ABIs. Commit the generated files. Now every contract call gets autocomplete and argument checks at compile time. This is usually a half-day of work with a payoff that lasts forever.

3. Centralize chain configuration

Make one chains.ts file. It exports your supported networks: their RPC URLs, contract addresses, and explorer links. Nothing hardcodes a chain ID, an RPC URL, or a contract address anywhere else. Adding a chain becomes adding one object to one array.

// chains.ts
export const supportedChains = {
  ethereum: {
    id: 1,
    name: 'Ethereum',
    rpcUrl: import.meta.env.VITE_ETH_RPC_URL,
    contracts: {
      token: '0x...',
    },
  },
  polygon: {
    id: 137,
    name: 'Polygon',
    rpcUrl: import.meta.env.VITE_POLYGON_RPC_URL,
    contracts: {
      token: '0x...',
    },
  },
} as const;

Every spot that used to hardcode a chain ID now imports from here. The work is mechanical: grep and replace. The result is a codebase where "add Arbitrum support" means editing one file.

4. Fix event listener lifecycle

Web3 event listeners that never get cleaned up are a common cause of memory leaks and duplicate transaction toasts. In React it looks like a useEffect that registers a Transfer event on a contract but never calls contract.off() in the cleanup. Navigate around the app a few times and these pile up.

useEffect(() => {
  const onTransfer = (from: string, to: string, amount: bigint) => {
    // handle event
  };
  contract.on('Transfer', onTransfer);

  return () => {
    contract.off('Transfer', onTransfer); // always clean up
  };
}, [contract]);

Go through every contract.on() call. Check that a matching contract.off() exists in a cleanup path.

5. Type your state properly

Web3 values have their own types. Address is a checksummed 0x string. Hash is a transaction hash. bigint holds all token amounts in ethers.js v6. Using plain string for all of these works, but it throws away the distinction. Use viem's or wagmi's native types, or even simple branded types. Then you can't pass a transaction hash where a contract address belongs.

How to Sell Refactoring to a Team That's Scared

Most pushback on refactoring isn't about the work. It's about fear. Fear of breaking something that runs. Fear of wasting time on code that "works." Fear of the deploy that takes down production three weeks after the refactor lands.

Answer each one straight.

"It works, why touch it?" Because the last three features took twice as long as you planned, and the next five will too. Cleaning up now is cheaper than building on a bad foundation. Use real numbers from your own velocity.

"We can't afford downtime." Refactoring the application layer does nothing to your deployed contracts. You're not touching on-chain code. The risk lives in the frontend build, and you can test that hard before you ship. Roll it out to 5% of users first and most of the rest of the risk is gone.

"We don't have time." Run it as a parallel track. The refactor doesn't stop feature work. You extract the wallet module while someone else ships the new feature. You move the chain config over in the background. Not everything needs a big rewrite in one shot.

The line that works best: "We spend one week cleaning this up now, or we spend two extra weeks on every major feature for the next year." That math lands.

What Refactoring Actually Looks Like in Practice

I cleaned up a DeFi platform last year. Before: four different ways to connect a wallet in different parts of the app, contract addresses hardcoded in at least twelve components, no typed ABIs, and a chainId conditional that had grown past 200 lines.

The work took three weeks, on a codebase that had been in production for two years. We didn't touch the contracts. We didn't rewrite the business logic. We pulled out and centralized the Web3 plumbing. That's it.

After: adding a chain took one afternoon. Velocity on new features went up in the first month. Not because the new code was clever, but because developers stopped being scared to open the files they'd learned to avoid.

That's what a good refactor feels like. Not pretty code for its own sake. Developers who feel safe making changes.

One Rule

Don't refactor and add a feature in the same PR. It makes the review impossible and the rollback a nightmare. Branch, refactor, test, merge. Then start the feature. That bit of discipline is the difference between a clean refactor and one that ships six new bugs.

Look at your own codebase for a second. How many of these symptoms does it have right now?


If your Web3 codebase has any of the symptoms above and you'd rather have someone who's done this a few times lead the work, I do code refactoring consulting. One engagement, fixed scope, no ongoing commitment required.