Blockchain Integration with Spring Boot: Building Decentralized Applications Using Web3j

A comprehensive guide to integrating Ethereum blockchain with Spring Boot applications using Web3j library, including smart contract development, deployment, and interaction patterns.

GT
Gonnect Team
January 14, 202415 min readView on GitHub
JavaSpring BootBlockchainWeb3jEthereumSoliditySmart Contracts

Introduction

The convergence of blockchain technology and traditional enterprise applications represents one of the most significant paradigm shifts in modern software development. While blockchain promises decentralization, immutability, and trustless transactions, integrating it with existing Java/Spring Boot ecosystems has traditionally been challenging.

Enter Web3j - a lightweight, highly modular Java library for working with smart contracts and integrating with Ethereum blockchain nodes. This article explores how to build decentralized applications (DApps) by combining the power of Spring Boot with Web3j and Ethereum.

Key Insight: Web3j enables Java developers to leverage their existing skills while tapping into the decentralized world of blockchain, eliminating the need to learn entirely new programming paradigms.

Understanding the Core Concepts

What is Web3j?

Web3j is a Java and Android library for integration with Ethereum clients. It provides:

FeatureDescription
JSON-RPC SupportComplete implementation of Ethereum JSON-RPC API
Smart Contract WrappersAuto-generated Java classes from Solidity contracts
Transaction ManagementSimplified transaction signing and submission
Filter SupportEvent and log filtering capabilities
Wallet SupportEthereum wallet creation and management

Blockchain Use Cases

The spring-boot-blockchain-web3j project demonstrates patterns applicable to various domains:

DomainUse Case
Financial ServicesDecentralized payment systems and settlements
HealthcareInteroperable health records and insurance claims
Supply ChainAsset tracking and provenance verification
AutomotiveVehicle history and fraud prevention
SubscriptionsDecentralized subscription management

Architecture Overview

The architecture combines Spring Boot's enterprise capabilities with Ethereum's decentralized infrastructure:

┌─────────────────────────────────────────────────────────────────┐
│                    Spring Boot Application                       │
│                         (REST API)                               │
└─────────────────────────────┬───────────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────────┐
│                      Web3j Library                               │
│              (Ethereum Java Integration)                         │
├─────────────────────────────────────────────────────────────────┤
│  • Transaction Management                                        │
│  • Smart Contract Wrappers                                       │
│  • Event Subscription                                            │
│  • Wallet Management                                             │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              │ JSON-RPC
                              │
┌─────────────────────────────▼───────────────────────────────────┐
│                    Ethereum Node (Geth)                          │
│                    (Rinkeby Testnet)                             │
├─────────────────────────────────────────────────────────────────┤
│  • Block Processing                                              │
│  • Transaction Validation                                        │
│  • Smart Contract Execution                                      │
│  • State Management                                              │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                   Ethereum Blockchain                            │
│              (Distributed Ledger Network)                        │
└─────────────────────────────────────────────────────────────────┘

Technology Stack

TechnologyPurpose
Spring BootApplication framework and REST API
Web3jEthereum blockchain integration
SoliditySmart contract language
GethGo Ethereum client for node operation
MavenBuild management with web3j plugin
Java 8Runtime environment (web3j requirement)

Important Note: The web3j Solidity compiler for contract generation requires Java 8. Java 9+ is not supported for this functionality.

Implementation Deep Dive

Setting Up the Ethereum Node

Before interacting with the blockchain, you need a running Ethereum node. Using Geth with the Rinkeby testnet:

# Start Geth with RPC enabled on Rinkeby testnet
geth --rpcapi personal,db,eth,net,web3 --rpc --rinkeby

This enables:

  • personal: Account management
  • db: Database operations
  • eth: Core Ethereum methods
  • net: Network information
  • web3: Web3-specific utilities

Maven Configuration

The project uses the web3j Maven plugin for generating Java wrappers from Solidity contracts:

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Web3j Core -->
    <dependency>
        <groupId>org.web3j</groupId>
        <artifactId>core</artifactId>
        <version>4.8.7</version>
    </dependency>

    <!-- Web3j Spring Boot Starter -->
    <dependency>
        <groupId>org.web3j</groupId>
        <artifactId>web3j-spring-boot-starter</artifactId>
        <version>4.8.7</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.web3j</groupId>
            <artifactId>web3j-maven-plugin</artifactId>
            <version>4.8.7</version>
            <configuration>
                <soliditySourceFiles>
                    <directory>src/main/resources/solidity</directory>
                    <includes>
                        <include>**/*.sol</include>
                    </includes>
                </soliditySourceFiles>
                <packageName>com.gonnect.blockchain.contracts</packageName>
                <outputDirectory>
                    ${project.build.directory}/generated-sources/web3j
                </outputDirectory>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>generate-sources</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Smart Contract Development

The project includes a voting smart contract written in Solidity:

// voting.sol
pragma solidity ^0.8.0;

contract Voting {
    // Candidate structure
    struct Candidate {
        uint id;
        string name;
        uint voteCount;
    }

    // Mapping of candidate ID to Candidate
    mapping(uint => Candidate) public candidates;

    // Track voters who have already voted
    mapping(address => bool) public voters;

    // Total number of candidates
    uint public candidatesCount;

    // Event emitted when a vote is cast
    event VotedEvent(uint indexed candidateId);

    // Add a new candidate
    function addCandidate(string memory _name) public {
        candidatesCount++;
        candidates[candidatesCount] = Candidate(
            candidatesCount,
            _name,
            0
        );
    }

    // Cast a vote
    function vote(uint _candidateId) public {
        // Require voter hasn't voted before
        require(!voters[msg.sender], "Already voted");

        // Require valid candidate
        require(
            _candidateId > 0 && _candidateId <= candidatesCount,
            "Invalid candidate"
        );

        // Record vote
        voters[msg.sender] = true;
        candidates[_candidateId].voteCount++;

        emit VotedEvent(_candidateId);
    }

    // Get candidate details
    function getCandidate(uint _candidateId)
        public view
        returns (uint, string memory, uint)
    {
        Candidate memory c = candidates[_candidateId];
        return (c.id, c.name, c.voteCount);
    }
}

Generating Java Contract Wrappers

Convert Solidity contracts to Java using the Maven plugin:

mvn web3j:generate-sources

This generates type-safe Java classes for contract interaction:

// Auto-generated Voting.java wrapper
public class Voting extends Contract {

    public static final String BINARY = "608060405234801561001057600080fd5b50...";

    public static final String FUNC_ADDCANDIDATE = "addCandidate";
    public static final String FUNC_VOTE = "vote";
    public static final String FUNC_GETCANDIDATE = "getCandidate";

    // Deploy contract
    public static RemoteCall<Voting> deploy(
            Web3j web3j,
            Credentials credentials,
            ContractGasProvider gasProvider) {
        return deployRemoteCall(
            Voting.class,
            web3j,
            credentials,
            gasProvider,
            BINARY,
            ""
        );
    }

    // Load existing contract
    public static Voting load(
            String contractAddress,
            Web3j web3j,
            Credentials credentials,
            ContractGasProvider gasProvider) {
        return new Voting(
            contractAddress,
            web3j,
            credentials,
            gasProvider
        );
    }

    // Add candidate method
    public RemoteCall<TransactionReceipt> addCandidate(String name) {
        final Function function = new Function(
            FUNC_ADDCANDIDATE,
            Arrays.asList(new Utf8String(name)),
            Collections.emptyList()
        );
        return executeRemoteCallTransaction(function);
    }

    // Vote method
    public RemoteCall<TransactionReceipt> vote(BigInteger candidateId) {
        final Function function = new Function(
            FUNC_VOTE,
            Arrays.asList(new Uint256(candidateId)),
            Collections.emptyList()
        );
        return executeRemoteCallTransaction(function);
    }

    // Get candidate method
    public RemoteCall<Tuple3<BigInteger, String, BigInteger>>
            getCandidate(BigInteger candidateId) {
        final Function function = new Function(
            FUNC_GETCANDIDATE,
            Arrays.asList(new Uint256(candidateId)),
            Arrays.asList(
                new TypeReference<Uint256>() {},
                new TypeReference<Utf8String>() {},
                new TypeReference<Uint256>() {}
            )
        );
        return executeRemoteCallMultipleValueReturn(function);
    }
}

Spring Boot Configuration

Configure Web3j connection in your Spring Boot application:

# application.yml
web3j:
  client-address: http://localhost:8545
  admin-client: true
  network-id: 4  # Rinkeby

blockchain:
  wallet:
    path: /path/to/wallet.json
    password: ${WALLET_PASSWORD}
  contract:
    gas-price: 20000000000
    gas-limit: 6721975
@Configuration
public class Web3jConfig {

    @Value("${web3j.client-address}")
    private String clientAddress;

    @Value("${blockchain.wallet.path}")
    private String walletPath;

    @Value("${blockchain.wallet.password}")
    private String walletPassword;

    @Bean
    public Web3j web3j() {
        return Web3j.build(new HttpService(clientAddress));
    }

    @Bean
    public Credentials credentials() throws Exception {
        return WalletUtils.loadCredentials(
            walletPassword,
            walletPath
        );
    }

    @Bean
    public ContractGasProvider gasProvider() {
        return new DefaultGasProvider();
    }
}

Service Layer Implementation

Create a service to interact with the smart contract:

@Service
public class VotingService {

    private final Web3j web3j;
    private final Credentials credentials;
    private final ContractGasProvider gasProvider;

    private Voting votingContract;

    public VotingService(
            Web3j web3j,
            Credentials credentials,
            ContractGasProvider gasProvider) {
        this.web3j = web3j;
        this.credentials = credentials;
        this.gasProvider = gasProvider;
    }

    // Deploy a new voting contract
    public String deployContract() throws Exception {
        votingContract = Voting.deploy(
            web3j,
            credentials,
            gasProvider
        ).send();

        return votingContract.getContractAddress();
    }

    // Load existing contract
    public void loadContract(String contractAddress) {
        votingContract = Voting.load(
            contractAddress,
            web3j,
            credentials,
            gasProvider
        );
    }

    // Add a candidate
    public TransactionReceipt addCandidate(String name)
            throws Exception {
        return votingContract.addCandidate(name).send();
    }

    // Cast a vote
    public TransactionReceipt vote(long candidateId)
            throws Exception {
        return votingContract.vote(
            BigInteger.valueOf(candidateId)
        ).send();
    }

    // Get candidate information
    public CandidateDTO getCandidate(long candidateId)
            throws Exception {
        Tuple3<BigInteger, String, BigInteger> result =
            votingContract.getCandidate(
                BigInteger.valueOf(candidateId)
            ).send();

        return new CandidateDTO(
            result.component1().longValue(),
            result.component2(),
            result.component3().longValue()
        );
    }

    // Get total candidates count
    public long getCandidatesCount() throws Exception {
        return votingContract.candidatesCount().send().longValue();
    }
}

REST Controller

Expose blockchain operations via REST API:

@RestController
@RequestMapping("/api/voting")
public class VotingController {

    private final VotingService votingService;

    public VotingController(VotingService votingService) {
        this.votingService = votingService;
    }

    @PostMapping("/deploy")
    public ResponseEntity<ContractResponse> deployContract()
            throws Exception {
        String address = votingService.deployContract();
        return ResponseEntity.ok(
            new ContractResponse(address, "Contract deployed")
        );
    }

    @PostMapping("/load/{address}")
    public ResponseEntity<String> loadContract(
            @PathVariable String address) {
        votingService.loadContract(address);
        return ResponseEntity.ok("Contract loaded: " + address);
    }

    @PostMapping("/candidates")
    public ResponseEntity<TransactionResponse> addCandidate(
            @RequestBody CandidateRequest request)
            throws Exception {

        TransactionReceipt receipt =
            votingService.addCandidate(request.getName());

        return ResponseEntity.ok(new TransactionResponse(
            receipt.getTransactionHash(),
            receipt.getBlockNumber().toString(),
            receipt.getGasUsed().toString()
        ));
    }

    @PostMapping("/vote/{candidateId}")
    public ResponseEntity<TransactionResponse> vote(
            @PathVariable long candidateId) throws Exception {

        TransactionReceipt receipt =
            votingService.vote(candidateId);

        return ResponseEntity.ok(new TransactionResponse(
            receipt.getTransactionHash(),
            receipt.getBlockNumber().toString(),
            receipt.getGasUsed().toString()
        ));
    }

    @GetMapping("/candidates/{id}")
    public ResponseEntity<CandidateDTO> getCandidate(
            @PathVariable long id) throws Exception {

        CandidateDTO candidate = votingService.getCandidate(id);
        return ResponseEntity.ok(candidate);
    }

    @GetMapping("/candidates/count")
    public ResponseEntity<Long> getCandidatesCount()
            throws Exception {
        return ResponseEntity.ok(
            votingService.getCandidatesCount()
        );
    }
}

Event Listening

Subscribe to blockchain events for real-time updates:

@Component
public class VotingEventListener {

    private static final Logger log =
        LoggerFactory.getLogger(VotingEventListener.class);

    private final Web3j web3j;

    public VotingEventListener(Web3j web3j) {
        this.web3j = web3j;
    }

    @EventListener(ApplicationReadyEvent.class)
    public void subscribeToEvents() {
        // Subscribe to new blocks
        web3j.blockFlowable(false)
            .subscribe(block -> {
                log.info("New block: {}",
                    block.getBlock().getNumber());
            });

        // Subscribe to pending transactions
        web3j.pendingTransactionFlowable()
            .subscribe(tx -> {
                log.info("Pending transaction: {}",
                    tx.getHash());
            });
    }

    // Subscribe to specific contract events
    public void subscribeToVotingEvents(String contractAddress) {
        // Filter for VotedEvent
        EthFilter filter = new EthFilter(
            DefaultBlockParameterName.EARLIEST,
            DefaultBlockParameterName.LATEST,
            contractAddress
        );

        web3j.ethLogFlowable(filter)
            .subscribe(eventLog -> {
                log.info("Vote event: block={}, tx={}",
                    eventLog.getBlockNumber(),
                    eventLog.getTransactionHash());
            });
    }
}

Running the Application

Prerequisites

  1. Java 8 (required for web3j contract generation)
  2. Maven 3.x
  3. Geth (Go Ethereum client)
  4. Test Ether (from Rinkeby faucet)

Setup and Run

# Clone the repository
git clone https://github.com/mgorav/spring-boot-blockchain-web3j.git
cd spring-boot-blockchain-web3j

# Start Geth node (in separate terminal)
geth --rpcapi personal,db,eth,net,web3 --rpc --rinkeby

# Generate contract wrappers
mvn web3j:generate-sources

# Build and run
mvn clean install
java -jar target/spring-boot-blockchain-web3j.jar

Testing the API

# Deploy the voting contract
curl -X POST http://localhost:8080/api/voting/deploy

# Response:
# {
#   "contractAddress": "0x1234...abcd",
#   "message": "Contract deployed"
# }

# Add candidates
curl -X POST http://localhost:8080/api/voting/candidates \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice"}'

curl -X POST http://localhost:8080/api/voting/candidates \
  -H "Content-Type: application/json" \
  -d '{"name": "Bob"}'

# Cast a vote
curl -X POST http://localhost:8080/api/voting/vote/1

# Get candidate results
curl http://localhost:8080/api/voting/candidates/1

# Response:
# {
#   "id": 1,
#   "name": "Alice",
#   "voteCount": 1
# }

Best Practices

Security Considerations

PracticeDescription
Private Key ManagementNever hardcode private keys; use environment variables or secure vaults
Gas EstimationAlways estimate gas before transactions to avoid failures
Input ValidationValidate all inputs before sending to blockchain
Access ControlImplement proper access control in smart contracts
Audit ContractsHave smart contracts audited before production deployment

Transaction Management

@Service
public class TransactionService {

    private final Web3j web3j;

    // Estimate gas before sending transaction
    public BigInteger estimateGas(
            String from,
            String to,
            String data) throws IOException {

        Transaction transaction = Transaction.createFunctionCallTransaction(
            from, null, null, null, to, data
        );

        EthEstimateGas estimate = web3j.ethEstimateGas(transaction).send();
        return estimate.getAmountUsed();
    }

    // Wait for transaction confirmation
    public TransactionReceipt waitForConfirmation(
            String txHash,
            int maxAttempts) throws Exception {

        TransactionReceiptProcessor processor =
            new PollingTransactionReceiptProcessor(
                web3j,
                TransactionManager.DEFAULT_POLLING_FREQUENCY,
                maxAttempts
            );

        return processor.waitForTransactionReceipt(txHash);
    }
}

Error Handling

@ControllerAdvice
public class BlockchainExceptionHandler {

    @ExceptionHandler(TransactionException.class)
    public ResponseEntity<ErrorResponse> handleTransactionException(
            TransactionException ex) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse(
                "TRANSACTION_FAILED",
                ex.getMessage()
            ));
    }

    @ExceptionHandler(ContractCallException.class)
    public ResponseEntity<ErrorResponse> handleContractException(
            ContractCallException ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse(
                "CONTRACT_ERROR",
                "Smart contract execution failed"
            ));
    }
}

Benefits and Trade-offs

Benefits

BenefitDescription
ImmutabilityTransaction history cannot be altered
TransparencyAll transactions are publicly verifiable
DecentralizationNo single point of failure
TrustTrustless transactions between parties
Java IntegrationLeverage existing Java/Spring expertise

Trade-offs

ChallengeMitigation
Transaction SpeedUse layer 2 solutions or sidechains
Gas CostsOptimize contract code; batch operations
Java VersionUse Java 8 for contract generation
ComplexityInvest in developer training
DebuggingUse testnets extensively; implement logging

Conclusion

Integrating blockchain with Spring Boot using Web3j opens up powerful possibilities for building decentralized applications while leveraging existing Java expertise. The spring-boot-blockchain-web3j repository demonstrates practical patterns for:

  • Smart Contract Integration: Converting Solidity contracts to type-safe Java wrappers
  • Transaction Management: Handling blockchain transactions with proper gas estimation
  • Event Subscription: Real-time monitoring of blockchain events
  • REST API Exposure: Making blockchain operations accessible via standard APIs

As blockchain technology continues to mature, the combination of enterprise Java frameworks with decentralized infrastructure will become increasingly relevant for building the next generation of trustless, transparent applications.


Further Reading