devoracles.

What is Web3 development: lessons from building dApps
Developer Tools & SDKs

What is Web3 development: lessons from building dApps

Most blockchain projects do not fail because the team cannot deploy a smart contract.

They fail later, when the contract has to coexist with unreliable RPC providers, delayed indexers, wallet-specific behavior, changing network conditions, and users who expect a transaction interface to explain what is happening in real time.

What is Web3 development, properly understood, has a deceptively familiar answer: building applications. The difference is that the application does not operate inside a controlled backend. Its critical state is written to an adversarial network, its users sign actions with their own keys, and its external dependencies can fail in ways that are visible, expensive, and difficult to reverse. Every component — from RPC endpoints to oracle data feeds — has to be modeled as an external party whose responses may be delayed, inconsistent, manipulated, or unavailable.

The general engineering lesson is straightforward: treating the dApp backend as a thin bridge between a frontend and a deployed smart contract produces systems that may be functionally correct on testnet but economically fragile under load and operationally brittle under adversarial conditions. Production readiness demands that the developer architect the off-chain stack with the same rigor conventionally applied to on-chain contracts.

The Reality of the Web3 Lifecycle: Beyond the MVP

A realistic production timeline for a DeFi or NFT platform spans several overlapping phases. Smart contract development takes place alongside product discovery, interface design, threat modeling, and decisions about custody, upgrades, indexing, and data availability. Frontend integration begins before the contract is considered final, because wallet connectors, transaction-signing flows, and event-driven interface states expose assumptions that are easy to miss in an isolated Solidity test.

In a Web2 product, the backend usually has an authoritative answer that the frontend can request. In a dApp, the interface is constantly reconciling several forms of state:

  • the wallet’s local account and network state;
  • the transaction’s status in the mempool;
  • the transaction status reported by the current RPC provider;
  • the transaction status on the canonical chain;
  • the indexed representation of emitted events;
  • the final state displayed to the user.

These are not interchangeable. A transaction can be signed but not broadcast, broadcast but not mined, mined but not yet considered final, or finalized on-chain while the indexer is still processing the resulting events. A frontend that collapses all of these states into “success” and “error” is not simplifying the system. It is hiding the conditions under which the system can fail.

That is why optimistic UI behavior has to be treated carefully. Showing a completed transfer immediately after a wallet signature may feel responsive, but it creates a false promise if the transaction is rejected, replaced, reverted, or affected by a reorganization. The product does not need to freeze after every click, but it does need to distinguish clearly between submitted, pending, confirmed, and finalized states.

A useful transaction interface answers different questions at different moments:

  • Has the user signed the action?
  • Has a node accepted the transaction for propagation?
  • Has the transaction entered a block?
  • Has the application waited long enough to treat that block as reliable?
  • Have the relevant events reached the indexer?
  • Does the current read model agree with the canonical chain?

A good transaction interface is not merely a loading spinner. It is a map of uncertainty.

Why the First Release Is Rarely the First System

The first release often contains more than one product. There is the contract system, the interface, the indexing path, the operational tooling, and the support process for transactions that do not behave as expected. Each has a different definition of “done.”

A contract may be complete from the perspective of its business logic while the deployment process remains unsafe. A frontend may display balances correctly for a clean wallet on a local fork but fail when the user switches networks during a pending transaction. An indexer may handle ordinary event flow while producing duplicate records after a restart. An oracle integration may return plausible values in normal conditions but leave the protocol without a defined response when the feed is stale.

Security auditing follows, often claiming two to three months for a competent third-party firm to identify reentrancy vulnerabilities, access-control failures, arithmetic edge cases, and flawed assumptions around external calls. That review is valuable, but it does not close the lifecycle. Findings may require contract changes; contract changes may invalidate earlier tests; integration with an oracle, lending protocol, bridge, or Layer 2 settlement layer may create risks that were absent from the isolated review.

What teams systematically underestimate is the entropy introduced at each phase boundary. A contract audited in isolation may exhibit emergent vulnerabilities when composed with an external oracle, a flash-loan aggregator, or another protocol that makes assumptions about timing and callback behavior. Those failures can remain invisible to unit testing and surface only under integrated network conditions, where multiple protocols share state and respond to one another’s timing.

The dapp development process is therefore not a linear pipeline. It is a recursive loop in which every external dependency introduces a new attack surface, a new liveness constraint, and a new reconciliation requirement between off-chain and on-chain state.

A smart contract that passes a security audit is not a system that has been secured; it is a component that has been inspected.

The practical consequence is that teams should test the whole transaction path earlier than they usually do. The contract, wallet adapter, RPC provider, indexer, oracle, and frontend should not meet for the first time during a release candidate. The earlier they are exercised together, the less likely it is that a seemingly minor interface mismatch becomes a production incident.

A mature test environment should reproduce more than successful calls. It should include dropped submissions, delayed receipts, stale reads, chain reorganizations, provider disagreement, rejected signatures, failed simulations, and indexer restarts. The point is not to predict every incident. It is to ensure that the system has a known behavior when the expected path stops being available.

Optimizing the Developer Stack: Foundry vs. Hardhat and Indexing Layers

The web3 developer stack has crystallized around a small number of dominant tools, each of which imposes architectural tradeoffs that propagate through the entire CI pipeline. Foundry, the Rust-based toolchain, is built for fast Solidity-native testing. Hardhat remains deeply comfortable for JavaScript and TypeScript-heavy workflows, deployment scripts, plugins, and application integration.

The difference is not cosmetic. At the scale of continuous integration pipelines running thousands of property-based tests against mainnet forks, compilation and execution speed compound into CI budgets measured in minutes rather than hours. For teams managing complex DeFi primitives with extensive fork testing, throughput determines whether nightly test runs remain routine or quietly disappear because developer time exceeds infrastructure cost.

Foundry is particularly effective when the core team wants Solidity to remain the center of the testing workflow. Fuzz tests, invariant tests, fork-based scenarios, and low-level execution traces can live close to the contracts they exercise. Hardhat remains a strong choice when the project depends heavily on TypeScript tooling, custom plugins, deployment scripts, or a broader JavaScript ecosystem. The decision is less about choosing the universally superior framework than about choosing the framework that reduces friction in the failure modes the team expects to investigate.

A useful comparison looks like this:

ConcernFoundryHardhat
Primary testing languageSolidityJavaScript or TypeScript, with Solidity support
StrengthFast compilation, fuzzing, invariant testing, and fork workflowsFlexible scripting, plugins, and TypeScript integration
Best fitContract-heavy teams and DeFi protocol developmentApplications with substantial JavaScript infrastructure
Main tradeoffSmaller surrounding JavaScript-oriented ecosystemMore runtime and configuration overhead in some test suites
Operational questionCan the team debug failures directly at the EVM level?Can the team integrate deployment and application tooling without custom glue?

The choice also affects onboarding and incident response. A team that writes most of its production behavior in TypeScript may gain speed from keeping deployment, simulation, and test utilities in the same language. A protocol team that spends its time reasoning about storage slots, invariant preservation, and adversarial state transitions may prefer a Solidity-centered workflow even if some application tooling remains elsewhere.

There is no requirement that the entire stack use one framework. A project can use Foundry for contract compilation, fuzzing, and invariant tests while retaining TypeScript scripts for deployment, frontend integration, and operational tasks. The cost of this hybrid approach is not technical impossibility; it is coordination. Compiler versions, artifact paths, network configuration, fork settings, and deployment addresses must remain consistent across tools. If those details are duplicated manually, the stack eventually develops two slightly different definitions of the same deployment.

Indexing Is a Read Model, Not a Second Blockchain

Read operations remain a bottleneck regardless of contract-level tooling choices. Querying the blockchain directly for user balances, transaction histories, or event logs introduces latency that is incompatible with a production-grade user experience. A single eth_call may return quickly under normal conditions and degrade during congestion or provider stress. Asking the chain to reconstruct a user’s complete activity history on every page load is also an inefficient use of the RPC layer.

The Graph, Ponder, and Subsquid have emerged as important indexing layers, each supporting a model in which events and relevant state are ingested into a queryable store. The frontend then reads from the indexed database rather than asking an RPC endpoint to perform repeated historical reconstruction. Depending on the implementation, that store may be PostgreSQL, a columnar database, or another persistence layer suited to the project’s query patterns.

Indexing does not make the chain disappear. It creates a read model that has to be monitored and reconciled. The indexer must handle missed blocks, chain reorganizations, duplicate event delivery, schema changes, and the difference between a transaction being included and a state being considered final.

A fast query against stale data is still wrong. The engineering task is not simply to add an indexer, but to define:

  • which data can be served from the read model;
  • which values must be checked directly against the chain;
  • how freshness is represented in the interface;
  • how historical backfills are performed;
  • how reorgs and duplicate events are reconciled;
  • what the application does when the indexer falls behind.

This is where many teams discover that smart contract infrastructure is only one part of the product. The protocol may be deterministic, but the surrounding data path is a distributed system. It needs health checks, replay procedures, alerting, and a clear response when the indexed state diverges from the chain.

The same principle applies to oracle data feeds. An oracle response is not simply another API result. The application needs to know whether the value is fresh, whether the feed has crossed a configured deviation threshold, whether the answer is valid for the current network, and what happens when updates stop. A protocol that consumes a price without defining the stale-data path has not completed its integration design.

The Cost of Convenience in Development Environments

Local development environments are excellent at removing friction. They are also excellent at hiding it. Local nodes provide fast mining, deterministic state, predictable fees, and no meaningful competition for block space. Those properties are useful for testing contract logic, but they should not be mistaken for a model of production.

Fork-based testing closes part of the gap by bringing real contract state and deployed dependencies into the test environment. It can reveal assumptions about token balances, approvals, liquidity, oracle configuration, and protocol composition. It still does not reproduce every property of a live network. Timing, provider behavior, mempool visibility, reorgs, and competing transactions remain operational concerns rather than purely local test concerns.

The most productive development stack is therefore not the one with the fewest tools. It is the one that makes the boundaries between tools visible. Contract tests should expose deployment artifacts. Deployment scripts should record addresses and configuration. Integration tests should exercise the same ABI and network settings used by the application. Indexing tests should verify both ordinary event flow and replay behavior. Operational dashboards should use identifiers that developers can trace back to a transaction, block, account, and provider response.

Architecting for Scale: RPC Failover and Nonce Management

Beneath the indexing layer lies the RPC infrastructure, which is the most underappreciated single point of failure in a typical Web3 application. A single hosted provider represents a centralized dependency in an otherwise decentralized stack. Its outage propagates immediately to wallet connections, balance reads, simulations, transaction submissions, and any server-side worker that depends on node access.

Production backends implement multi-provider failover, combining paid RPC services with self-hosted geth or reth nodes where the operational model justifies them. Endpoints can be rotated through circuit breakers that stop sending traffic to a provider after repeated failures. Some systems weight providers by recent latency and error rate; others reserve secondary endpoints for submission, historical reads, or emergency recovery.

The correct arrangement depends on the chain and the workload, but the principle is consistent: an RPC provider is infrastructure, not an infallible source of truth.

Failover also requires consistency checks. Two providers may answer the same request at different points during a reorganization or return different results when one is lagging behind the chain head. A backend that switches providers without recording which endpoint supplied each response can make debugging nearly impossible. For sensitive operations, the system should compare block references, verify that the provider is on the expected network, and distinguish a temporary transport failure from a semantic response such as a reverted transaction.

A useful provider policy separates operations by their tolerance for inconsistency:

  • Public reads can often use a latency-aware provider pool.
  • Historical queries may require an archive-capable endpoint.
  • Simulations should be tied to a recent block reference where possible.
  • Transaction submission should support controlled rebroadcasting.
  • Administrative actions should use a narrower, auditable path rather than whichever endpoint happens to be fastest.

Nonces Are an Application Concern

Transaction submission introduces a parallel set of failures that is unique to account-based blockchains and has no direct analog in Web2 backends. Concurrent submissions from a single signing address can be assigned the same nonce if the application relies on an outdated view of the account state. One submission may replace another, one may be rejected by the node, or transactions may be accepted by different providers with inconsistent views of the account’s pending nonce.

A nonce gap does not permanently strand user funds. It can, however, prevent later transactions from that account from being mined in sequence until the missing nonce is included, replaced, or otherwise resolved. The resulting delay can look like a frozen wallet to the user, particularly when the interface does not expose the pending transaction that is blocking subsequent actions.

Recovery may require rebroadcasting the missing transaction, replacing it with a transaction using the same nonce and a higher fee, or reconciling the local queue with the chain and mempool state. These are different operations. A replacement attempts to preserve the position in the account’s sequence. A new nonce creates a different transaction and does not unblock an earlier gap.

Production wallets and transaction services therefore need a queue that serializes submissions against a tracked nonce state. They also need explicit rules for retries. A retry is not always a new transaction: resubmitting with the same nonce may be a replacement attempt, while submitting with a new nonce creates a different operation. Confusing those two cases can produce duplicate actions or a queue that appears healthy while the account remains blocked.

The naive approach — broadcasting transactions in immediate response to user actions without nonce awareness — produces user-visible failures that no amount of frontend polish can mask.

A typical production transaction submission proceeds through the following sequence:

1. The user initiates an action in the frontend, which constructs and signs a transaction payload.

2. The wallet or backend queue assigns the submission against the tracked account nonce and rejects conflicting local operations.

3. The transaction is broadcast to the primary RPC provider with a fee strategy appropriate to current network conditions.

4. If the provider fails or the transaction remains pending beyond an operational threshold, the system checks whether to rebroadcast, replace, or wait rather than blindly creating another submission.

5. Once the transaction is included, the backend monitors confirmations according to the application’s risk tolerance.

6. The indexer ingests the resulting events and updates the local read store, while the frontend receives a state transition through polling, a subscription channel, or a server-side notification.

7. If a reorganization changes the transaction’s status or removes an event from the canonical chain, the indexer and application reconcile the affected state.

This sequence exposes the dependency chain that must be modeled explicitly: wallet state, nonce tracker, fee estimator, RPC pool, indexer pipeline, confirmation policy, and notification channel. Each link introduces its own failure mode, and each failure mode needs a deliberate response. Silently swallowed exceptions remain one of the common causes of production outages in this stack.

Failure ModeMechanismProduction Mitigation
RPC provider outageA centralized endpoint returns errors or times outMulti-provider pool with health checks and circuit breakers
Nonce collisionConcurrent submissions use the same account nonceSerialized queue with local nonce tracking and reconciliation
Nonce gapA later transaction waits behind an earlier missing or pending nonceMonitor sequence state and support same-nonce replacement or rebroadcast
Fee escalationA transaction remains pending as network conditions changeDynamic fee estimation and explicit replacement policy
ReorganizationA previously observed block is removed from the canonical chainFork-aware indexing and confirmation thresholds

Scaling is not only a matter of adding more RPC capacity. It means making transaction state observable. Every submission should have a traceable identifier, a known account, a nonce, a provider history, a current lifecycle state, and a recovery path. Without that information, support teams are forced to ask users for screenshots while engineers search several unrelated dashboards for a partial explanation.

Security Baselines: Fuzz Testing and the Cost of Audits

For any contract managing substantial user funds, fuzz testing with a meaningful run budget per function constitutes a serious security baseline rather than an aspirational extra. Property-based fuzzing — in which invariants are asserted across randomized inputs — surfaces edge cases that hand-written unit tests systematically miss: integer boundary conditions, unexpected call sequences, reentrancy through external interactions, and state corruption caused by malformed calldata.

Foundry’s built-in fuzzer has become a practical tool for this layer, integrating invariant testing directly into the contract suite. The value is not the number of random inputs by itself. The value comes from describing what must remain true while the system is subjected to sequences the author did not manually anticipate.

Useful invariants vary by protocol, but they often concern conservation and authorization:

  • balances should not be created or destroyed outside explicitly defined paths;
  • a user without the required role should not be able to alter privileged state;
  • collateralization or solvency conditions should not be bypassed through call ordering;
  • an oracle value outside its accepted freshness or validity rules should not be used for settlement;
  • a paused contract should not continue to execute operations that the pause is meant to stop;
  • accounting totals should remain consistent after deposits, withdrawals, liquidations, and failed calls.

Fuzzing is not a replacement for targeted tests. A fuzzer can find an input sequence that violates an invariant, but the team still has to understand the economic meaning of that violation. It is also not a replacement for manual review. Business logic can be internally consistent and still encode the wrong rule. A contract can preserve every balance invariant while allowing an unauthorized user to trigger an operation that the product never intended to expose.

What an Audit Can and Cannot Buy

An external audit is best understood as a concentrated review of assumptions, implementation, and attack surface. It can identify vulnerabilities, clarify ambiguous logic, and provide an independent view of the code. It cannot guarantee that the deployed bytecode matches the reviewed commit, that the operational keys are controlled safely, or that a future upgrade will preserve the same security properties.

Audit scope matters. A review of the core contract may not include deployment scripts, upgrade administration, oracle configuration, frontend signing logic, indexer behavior, or the interaction with a third-party protocol. These exclusions are not necessarily flaws in the audit. They are boundaries that the project must understand before using the audit as a public assurance signal.

The same caution applies to audit cost. A quote reflects more than lines of Solidity. It may depend on code maturity, documentation quality, number of integrations, upgradeability model, testing evidence, and the time available for remediation and verification. A rushed review of changing code creates a different result from a review of a stable system with reproducible deployments and documented invariants.

The expensive part of security is often not the audit invoice. It is the cost of changing an architectural decision after deployment. If a protocol discovers that an oracle can become stale, that an upgrade role is too powerful, or that a failed callback leaves accounting inconsistent, the remediation may involve migration procedures, paused functionality, user communication, and liquidity disruption.

Gas Optimization Comes After Correctness

Gas optimization belongs in the security conversation because aggressive optimization can obscure state transitions and make reviews harder. Packing storage, reducing external calls, caching values, and redesigning data structures can all improve execution costs. They can also introduce subtle assumptions about storage layout, type conversion, or the order in which effects become visible.

The correct sequence is to establish behavior, encode invariants, test failure paths, and measure actual execution before optimizing the hot paths. A cheaper function that is difficult to reason about is not automatically an improvement for a protocol holding user assets.

Security work also extends beyond Solidity. Signing flows should make domain separation and chain identity visible. Backend services should not become de facto custodians merely because they relay transactions. Deployment keys, upgrade keys, oracle administration, and emergency controls need separate policies and monitoring. The broader system should assume that one component will eventually be compromised or unavailable and limit the damage that follows.

Security is not a milestone on the dApp roadmap. It is the set of constraints that makes the roadmap survivable.

The visible budget for a Web3 product is usually the engineering team. The less visible budget includes infrastructure, monitoring, audits, legal advice, incident response, compliance work, and the time required to operate a system whose users hold their own assets.

A dApp may not have a conventional custody model, but that does not remove legal questions. The answers depend on the product’s functions, jurisdictions, user base, token design, interface, marketing claims, and relationship to financial activity. A protocol that merely publishes immutable software presents a different set of questions from a service that controls upgrades, routes orders, collects fees, or operates a hosted account system.

Legal work should begin before the architecture is frozen. The choice between permissionless and permissioned access, the handling of personal data, the use of third-party identity providers, the structure of a token or rewards program, and the location of operational entities can all affect implementation. Retrofitting those decisions after contracts, interfaces, and distribution plans are public is expensive and sometimes impossible without changing the product.

A realistic budget should account for several categories:

  • contract and application engineering;
  • RPC, node, indexing, database, and observability infrastructure;
  • security reviews, remediation, and verification;
  • legal structuring and jurisdiction-specific advice;
  • documentation, support, and incident communication;
  • deployment operations, key management, and emergency procedures;
  • ongoing maintenance when dependencies change.

Infrastructure costs also behave differently from ordinary SaaS costs. A sudden increase in usage can raise RPC demand, indexer load, database storage, and support volume at the same time. A network migration may require new providers, new indexing logic, new contract deployments, and a new testing matrix. A third-party protocol upgrade can create work even when the dApp’s own code has not changed.

The budget should therefore include operational slack rather than assuming that every month resembles the development environment. A team that spends its entire budget reaching launch may have no capacity left to investigate a provider outage, respond to a vulnerability disclosure, reproduce a transaction dispute, or update an integration after a dependency changes.

Build Versus Buy Is Also a Risk Decision

Using a managed RPC provider or indexing service can be the right choice. It reduces operational burden and lets a small team focus on product behavior. It also creates dependency on a vendor’s availability, rate limits, data model, support process, and pricing.

Self-hosting provides greater control over node behavior and data access, but it moves maintenance into the team’s budget. Nodes require upgrades, storage planning, monitoring, network knowledge, and recovery procedures. An organization should not self-host infrastructure merely to make a decentralization claim if it cannot operate that infrastructure reliably.

The same calculation applies to indexers, wallets, analytics, notification services, and oracle providers. The question is not whether a dependency is centralized. Many useful dependencies are. The question is whether the team has documented the dependency, assessed the failure mode, and built an exit or fallback path proportionate to the product’s risk.

A sensible architecture records those decisions explicitly:

DecisionQuestion to Answer
RPC providerWhat continues to work if the primary endpoint is unavailable or lagging?
Indexing layerHow are missed events, reorgs, and historical backfills handled?
Oracle feedWhat happens when the value is stale, invalid, or unavailable?
Transaction queueWho owns nonce state, and how are replacements authorized?
Upgrade controlsWhich roles can change code or configuration, and how are they monitored?
Legal structureWhich product functions create obligations in the target jurisdictions?
Incident responseWho can pause, communicate, investigate, and restore service?

These are not documents for a future operations department. In a small Web3 team, the same people often write the contract, manage the deployment, answer support requests, and decide whether an emergency control should be used. Making the assumptions explicit is a way to reduce the number of decisions that must be improvised during an incident.

What Web3 Development Really Means

The web3 coding basics are easy to list: write a contract, connect a wallet, submit a transaction, and read the resulting state. The difficult part is understanding what those steps imply once the application leaves the local node.

Web3 development is systems engineering under public execution. The application’s state is shared, the users control signing, dependencies fail in observable ways, and mistakes can become irreversible. The developer stack must account for testing speed, fork-based integration, indexed reads, RPC redundancy, nonce serialization, oracle freshness, confirmation policy, and security review. The dapp development process is successful only when these pieces behave coherently under conditions that are less convenient than the happy path.

The strongest teams do not treat infrastructure as an afterthought to the contract. They treat the contract, transaction lifecycle, data feeds, indexer, RPC layer, frontend, and operational controls as one system with different trust boundaries. That is the practical difference between deploying a demo and building a dApp that can survive real users.

FAQ

Why do most blockchain projects fail after deployment?
Projects often fail because they struggle to coexist with unreliable external dependencies like RPC providers, delayed indexers, and changing network conditions, rather than failing due to smart contract code itself.
How should a dApp handle transaction states in the user interface?
The interface should clearly distinguish between submitted, pending, confirmed, and finalized states to avoid creating false promises, as a transaction can be signed but not yet mined or finalized.
What is the main difference between Foundry and Hardhat?
Foundry is a Rust-based toolchain optimized for fast Solidity-native testing and invariant workflows, while Hardhat is better suited for projects heavily reliant on TypeScript, custom plugins, and JavaScript-based infrastructure.
Why is indexing necessary for dApp development?
Querying the blockchain directly for historical data is inefficient and slow; indexing layers like The Graph or Ponder create a queryable read model that improves performance and user experience.
What are the risks of using a single RPC provider?
A single RPC provider acts as a centralized point of failure; if it goes down or experiences latency, the entire application's ability to read state or submit transactions is compromised.
What is the purpose of fuzz testing in Web3?
Fuzz testing asserts invariants across randomized inputs to uncover edge cases like integer boundary conditions or state corruption that manual unit tests typically miss.