<\!DOCTYPE html> API Development - Dione Developer Course

🎯 What You'll Learn

  • How Dione's SOAP-based API architecture actually works
  • Real WSDL contracts and service implementations
  • Authentication patterns and session management
  • Business API endpoints and their quirks
  • Debugging techniques and error handling

SOAP Services Reality

⚠️ No REST Here

Dione is a SOAP-only system. Every API endpoint is SOAP-based using Apache CXF. There are no REST endpoints, no JSON APIs, and no GraphQL. Understanding this is crucial.

Why SOAP in 2024?

Before you question this choice, understand the business context:

  • Financial Industry Standard: When Dione was built, SOAP was the financial services standard
  • Strong Typing: WSDL contracts provide compile-time safety for financial transactions
  • Compliance Requirements: Auditors understand SOAP contracts better than REST documentation
  • Tooling: Financial messaging standards (ISO 20022, FIX) have better SOAP tooling
  • Error Handling: SOAP faults provide structured error information

Service Architecture Overview

SOAP Service Layer Architecture
┌─────────────────────────────────────┐
│          Client Applications        │
│    (Customer Portal, Admin Panel)   │
└─────────────────────────────────────┘
                  │ HTTP/SOAP
                  ▼
┌─────────────────────────────────────┐
│        Apache CXF Endpoint          │
│     (service-definition-beans.xml)  │
└─────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│       Service Implementation        │
│    (AuthServiceIMPL, CustomerIMPL)  │
└─────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│         Handler Layer               │
│   (Business Logic + Validation)     │
└─────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│          DAO Layer                  │
│     (Database Operations)           │
└─────────────────────────────────────┘

Service Endpoints Overview

All SOAP services are defined in the CXF configuration and deployed on JBoss. Here's what's actually available:

services/business/WebContent/WEB-INF/service-definition-beans.xml
<!-- Authentication & Session Management -->
<jaxws:endpoint id="auth" 
    implementor="com.currenciesdirect.gtg.ngop.business.services.auth.AuthServiceIMPL"
    address="/authServices" 
    serviceName="ns1:authService"
    endpointName="ns1:authPort"
    xmlns:ns1="http://com.currenciesdirect.gtg.ngop.business.services.auth/" />

<!-- Customer Management -->
<jaxws:endpoint id="customer"
    implementor="com.currenciesdirect.gtg.ngop.business.services.customer.CustomerManagementIMPL"
    address="/customerServices" 
    serviceName="ns2:customerService"
    endpointName="ns2:customerPort"
    xmlns:ns2="http://com.currenciesdirect.gtg.ngop.business.services.customer/" />

<!-- Payment Processing -->
<jaxws:endpoint id="payment"
    implementor="com.currenciesdirect.gtg.ngop.business.services.pfx.payment.PaymentManagementIMPL"
    address="/paymentServices"
    serviceName="ns3:paymentService"
    endpointName="ns3:paymentPort"
    xmlns:ns3="http://com.currenciesdirect.gtg.ngop.business.services.pfx.payment/" />

<!-- FX Trading -->
<jaxws:endpoint id="fxTrading"
    implementor="com.currenciesdirect.gtg.ngop.business.services.pfx.fxtrading.FXTradingIMPL"
    address="/fxTradingServices"
    serviceName="ns4:fxTradingService"
    endpointName="ns4:fxTradingPort"
    xmlns:ns4="http://com.currenciesdirect.gtg.ngop.business.services.pfx.fxtrading/" />

<!-- Open Banking -->
<jaxws:endpoint id="openBanking"
    implementor="com.currenciesdirect.gtg.ngop.business.services.pfx.openbanking.OpenBankingIMPL"
    address="/openBankingServices"
    serviceName="ns5:openBankingService"
    endpointName="ns5:openBankingPort"
    xmlns:ns5="http://com.currenciesdirect.gtg.ngop.business.services.pfx.openbanking/" />

Service URL Pattern

All services follow this URL pattern:

Service URLs
# Development Environment
http://localhost:8080/Dione/services/{serviceName}

# Production Environment
https://api.currenciesdirect.com/Dione/services/{serviceName}

# Actual Service Endpoints
http://localhost:8080/Dione/services/authServices?wsdl
http://localhost:8080/Dione/services/customerServices?wsdl
http://localhost:8080/Dione/services/paymentServices?wsdl
http://localhost:8080/Dione/services/fxTradingServices?wsdl

WSDL Contracts & Client Generation

Generating Client Stubs

To consume Dione services, you generate client stubs from WSDL contracts:

Maven CXF Plugin Configuration
<plugin>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-codegen-plugin</artifactId>
    <version>3.3.0</version>
    <executions>
        <execution>
            <id>generate-sources</id>
            <phase>generate-sources</phase>
            <configuration>
                <wsdlOptions>
                    <wsdlOption>
                        <wsdl>http://localhost:8080/Dione/services/authServices?wsdl</wsdl>
                        <packagenames>
                            <packagename>com.currenciesdirect.client.auth</packagename>
                        </packagenames>
                    </wsdlOption>
                    <wsdlOption>
                        <wsdl>http://localhost:8080/Dione/services/customerServices?wsdl</wsdl>
                        <packagenames>
                            <packagename>com.currenciesdirect.client.customer</packagename>
                        </packagenames>
                    </wsdlOption>
                </wsdlOptions>
            </configuration>
            <goals>
                <goal>wsdl2java</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Client Invocation Pattern

ServiceInvoker Pattern
@Component
public class AuthServiceInvoker {
    
    private WSAuth authService;
    
    @PostConstruct
    public void initializeService() {
        try {
            // Create service from WSDL
            URL wsdlLocation = new URL("http://localhost:8080/Dione/services/authServices?wsdl");
            AuthService service = new AuthService(wsdlLocation);
            this.authService = service.getAuthPort();
            
            // Configure timeouts
            Client client = ClientProxy.getClient(authService);
            HTTPConduit conduit = (HTTPConduit) client.getConduit();
            HTTPClientPolicy policy = new HTTPClientPolicy();
            policy.setConnectionTimeout(30000);  // 30 seconds
            policy.setReceiveTimeout(60000);     // 60 seconds
            conduit.setClient(policy);
            
        } catch (Exception e) {
            LOG.error("Failed to initialize auth service", e);
            throw new RuntimeException("Cannot connect to authentication service");
        }
    }
    
    public AuthResponse loginCustomer(String email, String password, String orgCode) {
        try {
            // Build request
            AuthRequest request = new AuthRequest();
            request.setEmail(email);
            request.setPassword(password);
            request.setOrgCode(orgCode);
            
            // Call service
            Holder<ResponseStatusHeader> statusHeader = new Holder<>();
            AuthResponse response = authService.login(statusHeader, request, "en", "WEB");
            
            // Check for errors
            if (statusHeader.value != null && !"SUCCESS".equals(statusHeader.value.getStatus())) {
                throw new ServiceException(statusHeader.value.getMessage());
            }
            
            return response;
            
        } catch (Exception e) {
            LOG.error("Login failed for user: " + email, e);
            throw new ServiceException("Authentication failed: " + e.getMessage());
        }
    }
}

Authentication API

Login Operation

The most-used API endpoint. Understanding its quirks is essential:

AuthServiceIMPL.java - login() method
@WebService(serviceName = "authService", portName = "authPort", 
            targetNamespace = "http://com.currenciesdirect.gtg.ngop.business.services.auth/")
public class AuthServiceIMPL implements WSAuth {
    
    @Autowired
    private LoginAndProfileHandlerInterface loginAndProfileHandlerRef;
    
    @Override
    public AuthResponse login(Holder<ResponseStatusHeader> responseStatusHeader,
                             AuthRequest authRequest, String locale, String source) {
        
        LOG.info("Login attempt for user: {} org: {} source: {}", 
                authRequest.getEmail(), authRequest.getOrgCode(), source);
        
        try {
            // Business logic delegation
            PojoMap sessionData = loginAndProfileHandlerRef.createNGOPSession(
                responseStatusHeader, authRequest, locale, source);
            
            // Build successful response
            AuthResponse response = new AuthResponse();
            response.setSessionID(sessionData.getString("sessionID"));
            response.setCustomerID(sessionData.getInteger("customerID"));
            response.setOrgID(sessionData.getInteger("orgID"));
            response.setProfile(buildCustomerProfile(sessionData));
            
            // Success status
            ResponseStatusHeader successHeader = new ResponseStatusHeader();
            successHeader.setStatus("SUCCESS");
            successHeader.setMessage("Login successful");
            responseStatusHeader.value = successHeader;
            
            return response;
            
        } catch (NGOPBaseException e) {
            // Business exception (invalid credentials, etc.)
            LOG.warn("Login failed for {}: {}", authRequest.getEmail(), e.getMessage());
            
            ResponseStatusHeader errorHeader = new ResponseStatusHeader();
            errorHeader.setStatus("BUSINESS_ERROR");
            errorHeader.setMessage(e.getMessage());
            errorHeader.setErrorCode(e.getErrorCode());
            responseStatusHeader.value = errorHeader;
            
            return new AuthResponse(); // Empty response on error
            
        } catch (Exception e) {
            // System exception
            LOG.error("System error during login for " + authRequest.getEmail(), e);
            
            ResponseStatusHeader errorHeader = new ResponseStatusHeader();
            errorHeader.setStatus("SYSTEM_ERROR");
            errorHeader.setMessage("Internal system error");
            responseStatusHeader.value = errorHeader;
            
            return new AuthResponse();
        }
    }
    
    private CustomerProfile buildCustomerProfile(PojoMap sessionData) {
        CustomerProfile profile = new CustomerProfile();
        profile.setFirstName(sessionData.getString("firstName"));
        profile.setLastName(sessionData.getString("lastName"));
        profile.setEmail(sessionData.getString("email"));
        profile.setKycStatus(sessionData.getString("kycStatus"));
        profile.setAccountStatus(sessionData.getString("accountStatus"));
        return profile;
    }
}

Session Management

Session Validation Pattern
@Override
public ValidateSessionResponse validateSession(Holder<ResponseStatusHeader> responseStatusHeader,
                                              ValidateSessionRequest request) {
    
    String sessionId = request.getSessionID();
    
    try {
        // Get session from cache
        PojoMap sessionData = cacheDataConsumer.get(sessionId);
        
        if (sessionData == null) {
            throw new NGOPBaseException("Session expired or invalid", "SESSION_INVALID");
        }
        
        // Check session timeout
        Long lastActivity = sessionData.getLong("lastActivity");
        if (System.currentTimeMillis() - lastActivity > SESSION_TIMEOUT) {
            cacheDataConsumer.remove(sessionId);
            throw new NGOPBaseException("Session timeout", "SESSION_TIMEOUT");
        }
        
        // Update last activity
        sessionData.put("lastActivity", System.currentTimeMillis());
        cacheDataConsumer.put(sessionId, sessionData, SESSION_TIMEOUT);
        
        // Build response
        ValidateSessionResponse response = new ValidateSessionResponse();
        response.setCustomerID(sessionData.getInteger("customerID"));
        response.setOrgID(sessionData.getInteger("orgID"));
        response.setValid(true);
        
        return response;
        
    } catch (NGOPBaseException e) {
        ValidateSessionResponse response = new ValidateSessionResponse();
        response.setValid(false);
        response.setReason(e.getMessage());
        return response;
    }
}

Customer Management API

Key Operations

  • getCustomerProfile: Retrieve complete customer information
  • updateCustomerProfile: Modify customer details
  • getCustomerAccounts: List customer's currency accounts
  • customerKYCUpdate: Update Know Your Customer status
Customer Profile API Implementation
@Override
public GetCustomerProfileResponse getCustomerProfile(
        Holder<ResponseStatusHeader> responseStatusHeader,
        GetCustomerProfileRequest request) {
    
    Integer customerId = request.getCustomerID();
    String sessionId = request.getSessionID();
    
    try {
        // Validate session
        PojoMap sessionData = validateSessionAndGetData(sessionId);
        
        // Security check - ensure customer can only access their own data
        Integer sessionCustomerId = sessionData.getInteger("customerID");
        if (!customerId.equals(sessionCustomerId)) {
            throw new NGOPBaseException("Access denied", "ACCESS_DENIED");
        }
        
        // Get customer from database
        Customer customer = customerDAO.getCustomerById(customerId);
        if (customer == null) {
            throw new NGOPBaseException("Customer not found", "CUSTOMER_NOT_FOUND");
        }
        
        // Transform to response object
        GetCustomerProfileResponse response = new GetCustomerProfileResponse();
        response.setCustomer(transformCustomerToProfile(customer));
        
        // Get customer accounts
        List<Account> accounts = accountDAO.getAccountsByCustomerId(customerId);
        response.setAccounts(transformAccountsToProfileAccounts(accounts));
        
        return response;
        
    } catch (NGOPBaseException e) {
        handleBusinessError(responseStatusHeader, e);
        return new GetCustomerProfileResponse();
    }
}

private CustomerProfile transformCustomerToProfile(Customer customer) {
    CustomerProfile profile = new CustomerProfile();
    profile.setCustomerID(customer.getCustomerId());
    profile.setFirstName(customer.getFirstName());
    profile.setLastName(customer.getLastName());
    profile.setEmail(customer.getEmail());
    profile.setPhone(customer.getPhone());
    profile.setDateOfBirth(customer.getDateOfBirth());
    profile.setKycStatus(customer.getKycStatus());
    profile.setAccountStatus(customer.getAccountStatus());
    
    // Address information
    if (customer.getAddress() != null) {
        Address address = new Address();
        address.setLine1(customer.getAddress().getLine1());
        address.setLine2(customer.getAddress().getLine2());
        address.setCity(customer.getAddress().getCity());
        address.setPostcode(customer.getAddress().getPostcode());
        address.setCountry(customer.getAddress().getCountry());
        profile.setAddress(address);
    }
    
    return profile;
}

Payment Processing API

Create Payment Flow

The payment API is the most complex because it orchestrates multiple systems:

PaymentManagementIMPL.java - createPayment() method
@Override
public CreatePaymentResponse createPayment(
        Holder<ResponseStatusHeader> responseStatusHeader,
        CreatePaymentRequest request) {
    
    LOG.info("Creating payment: {} {} {} → {} for customer {}", 
            request.getAmount(), request.getFromCurrency(), 
            request.getToCurrency(), request.getCustomerID());
    
    try {
        // 1. Validate session
        PojoMap sessionData = validateSession(request.getSessionID());
        
        // 2. Validate payment request
        PaymentValidationResult validation = paymentValidationHandler.validatePaymentRequest(
            request, sessionData);
        
        if (!validation.isValid()) {
            throw new NGOPBaseException(validation.getErrorMessage(), "VALIDATION_ERROR");
        }
        
        // 3. Get exchange rate from pricing engine
        FxRate exchangeRate = pricingEngineService.getExchangeRate(
            request.getFromCurrency(), request.getToCurrency(), 
            request.getAmount(), sessionData.getInteger("orgID"));
        
        // 4. Create payment record
        Payment payment = new Payment();
        payment.setCustomerId(request.getCustomerID());
        payment.setFromCurrency(request.getFromCurrency());
        payment.setToCurrency(request.getToCurrency());
        payment.setFromAmount(request.getAmount());
        payment.setExchangeRate(exchangeRate.getRate());
        payment.setToAmount(request.getAmount().multiply(exchangeRate.getRate()));
        payment.setStatus("PENDING");
        payment.setCreatedDate(new Date());
        payment.setCreatedBy(sessionData.getInteger("customerID"));
        
        // 5. Persist payment
        payment = paymentDAO.save(payment);
        
        // 6. Submit to payment processor (Titan)
        TitanPaymentRequest titanRequest = buildTitanRequest(payment, request);
        TitanPaymentResponse titanResponse = titanPaymentService.submitPayment(titanRequest);
        
        // 7. Update payment with processor response
        payment.setProcessorReferenceId(titanResponse.getReferenceId());
        payment.setProcessorStatus(titanResponse.getStatus());
        payment = paymentDAO.update(payment);
        
        // 8. Build response
        CreatePaymentResponse response = new CreatePaymentResponse();
        response.setPaymentID(payment.getPaymentId());
        response.setStatus(payment.getStatus());
        response.setExchangeRate(payment.getExchangeRate());
        response.setToAmount(payment.getToAmount());
        response.setEstimatedSettlement(calculateSettlementDate(payment));
        
        return response;
        
    } catch (NGOPBaseException e) {
        LOG.warn("Payment creation failed: {}", e.getMessage());
        handleBusinessError(responseStatusHeader, e);
        return new CreatePaymentResponse();
        
    } catch (Exception e) {
        LOG.error("System error creating payment", e);
        handleSystemError(responseStatusHeader, "Payment creation failed");
        return new CreatePaymentResponse();
    }
}

private TitanPaymentRequest buildTitanRequest(Payment payment, CreatePaymentRequest request) {
    TitanPaymentRequest titanRequest = new TitanPaymentRequest();
    titanRequest.setAmount(payment.getFromAmount());
    titanRequest.setCurrency(payment.getFromCurrency());
    titanRequest.setCustomerReference(payment.getPaymentId().toString());
    
    // Beneficiary details
    titanRequest.setBeneficiaryName(request.getBeneficiaryName());
    titanRequest.setBeneficiaryAccount(request.getBeneficiaryAccount());
    titanRequest.setBeneficiaryBank(request.getBeneficiaryBank());
    
    return titanRequest;
}

FX Trading API

Real-Time Rate Quotes

FX Rate Quote Implementation
@Override
public GetRateQuoteResponse getRateQuote(
        Holder<ResponseStatusHeader> responseStatusHeader,
        GetRateQuoteRequest request) {
    
    try {
        // Validate session
        validateSession(request.getSessionID());
        
        // Get live rate from pricing engine
        FxRate rate = pricingEngineService.getLiveRate(
            request.getFromCurrency(), 
            request.getToCurrency(),
            request.getAmount(),
            request.getOrgID());
        
        // Apply customer-specific margins
        BigDecimal customerRate = applyCustomerMargin(rate.getRate(), 
            request.getCustomerID(), request.getFromCurrency());
        
        // Build response
        GetRateQuoteResponse response = new GetRateQuoteResponse();
        response.setFromCurrency(request.getFromCurrency());
        response.setToCurrency(request.getToCurrency());
        response.setFromAmount(request.getAmount());
        response.setExchangeRate(customerRate);
        response.setToAmount(request.getAmount().multiply(customerRate));
        response.setQuoteExpiry(calculateQuoteExpiry()); // 30 seconds
        response.setQuoteReference(generateQuoteReference());
        
        // Cache quote for later execution
        cacheService.putQuote(response.getQuoteReference(), response, 30000);
        
        return response;
        
    } catch (Exception e) {
        LOG.error("Failed to get rate quote", e);
        handleSystemError(responseStatusHeader, "Rate service unavailable");
        return new GetRateQuoteResponse();
    }
}

private BigDecimal applyCustomerMargin(BigDecimal baseRate, Integer customerId, String currency) {
    // Get customer tier (determines margin)
    CustomerTier tier = customerDAO.getCustomerTier(customerId);
    
    // Get currency-specific margin
    BigDecimal margin = marginConfigService.getMargin(tier, currency);
    
    // Apply margin (add for buy, subtract for sell)
    return baseRate.multiply(BigDecimal.ONE.add(margin));
}

API Debugging & Testing

SOAP UI Testing

SoapUI is your best friend for testing Dione APIs:

SoapUI Test Setup
<!-- Sample Login Request -->
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
                  xmlns:auth="http://com.currenciesdirect.gtg.ngop.business.services.auth/">
   <soapenv:Header/>
   <soapenv:Body>
      <auth:login>
         <auth:responseStatusHeader/>
         <auth:authRequest>
            <auth:email>test@currenciesdirect.com</auth:email>
            <auth:password>password123</auth:password>
            <auth:orgCode>CD</auth:orgCode>
         </auth:authRequest>
         <auth:locale>en</auth:locale>
         <auth:source>WEB</auth:source>
      </auth:login>
   </soapenv:Body>
</soapenv:Envelope>

Logging and Monitoring

API Request Logging
<!-- logback.xml configuration for API debugging -->
<logger name="com.currenciesdirect.gtg.ngop.business.services" level="DEBUG"/>
<logger name="org.apache.cxf" level="INFO"/>
<logger name="org.apache.cxf.interceptor" level="DEBUG"/>

<!-- Enable SOAP message logging -->
<logger name="org.apache.cxf.services" level="DEBUG"/>

Common Debugging Commands

Useful Debugging Commands
# Test WSDL accessibility
curl -v "http://localhost:8080/Dione/services/authServices?wsdl"

# Check service health
curl -X POST -H "Content-Type: text/xml" \
  -d @login-request.xml \
  "http://localhost:8080/Dione/services/authServices"

# Monitor JBoss logs for API calls
tail -f /opt/jboss/standalone/log/server.log | grep "AuthServiceIMPL"

# Check database for recent API activity
psql -U dione_user -d dione_db -c \
  "SELECT * FROM audit_log WHERE operation_type = 'API_CALL' ORDER BY created_date DESC LIMIT 10;"

# Verify cache entries for sessions
echo "GET session:12345" | redis-cli

Error Handling Patterns

Standard Error Response Format

All Dione SOAP services use a consistent error handling pattern:

Error Response Pattern
// Standard error handling in all service implementations
public class BaseServiceImpl {
    
    protected void handleBusinessError(Holder<ResponseStatusHeader> responseStatusHeader, 
                                      NGOPBaseException e) {
        ResponseStatusHeader errorHeader = new ResponseStatusHeader();
        errorHeader.setStatus("BUSINESS_ERROR");
        errorHeader.setMessage(e.getMessage());
        errorHeader.setErrorCode(e.getErrorCode());
        errorHeader.setTimestamp(new Date());
        responseStatusHeader.value = errorHeader;
        
        LOG.warn("Business error: {} ({})", e.getMessage(), e.getErrorCode());
    }
    
    protected void handleSystemError(Holder<ResponseStatusHeader> responseStatusHeader, 
                                   String message) {
        ResponseStatusHeader errorHeader = new ResponseStatusHeader();
        errorHeader.setStatus("SYSTEM_ERROR");
        errorHeader.setMessage(message);
        errorHeader.setErrorCode("SYS_ERROR");
        errorHeader.setTimestamp(new Date());
        responseStatusHeader.value = errorHeader;
        
        LOG.error("System error: {}", message);
    }
}

Common Error Codes

Error Code Description Resolution
AUTH_FAILED Invalid credentials Check email/password combination
SESSION_INVALID Session expired or not found Login again to get new session
VALIDATION_ERROR Request validation failed Check required fields and formats
INSUFFICIENT_FUNDS Customer account balance too low Customer needs to fund account
RATE_EXPIRED FX rate quote has expired Get new rate quote
EXTERNAL_SERVICE_ERROR Downstream service unavailable Retry later or check service status

Client-Side Error Handling

Robust Client Error Handling
public class RobustServiceClient {
    
    public AuthResponse loginWithRetry(String email, String password, String orgCode) {
        int maxRetries = 3;
        int delay = 1000; // 1 second
        
        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                Holder<ResponseStatusHeader> statusHeader = new Holder<>();
                AuthRequest request = buildAuthRequest(email, password, orgCode);
                AuthResponse response = authService.login(statusHeader, request, "en", "WEB");
                
                // Check response status
                if (statusHeader.value != null) {
                    String status = statusHeader.value.getStatus();
                    
                    if ("SUCCESS".equals(status)) {
                        return response;
                    } else if ("BUSINESS_ERROR".equals(status)) {
                        // Don't retry business errors
                        throw new BusinessException(statusHeader.value.getMessage(), 
                                                   statusHeader.value.getErrorCode());
                    } else if ("SYSTEM_ERROR".equals(status)) {
                        LOG.warn("System error on attempt {}: {}", attempt, statusHeader.value.getMessage());
                        if (attempt == maxRetries) {
                            throw new SystemException("Service unavailable after " + maxRetries + " attempts");
                        }
                        // Wait before retry
                        Thread.sleep(delay * attempt);
                        continue;
                    }
                }
                
                return response;
                
            } catch (BusinessException e) {
                // Don't retry business errors
                throw e;
            } catch (Exception e) {
                LOG.warn("Connection error on attempt {}: {}", attempt, e.getMessage());
                if (attempt == maxRetries) {
                    throw new SystemException("Service unavailable: " + e.getMessage());
                }
                try {
                    Thread.sleep(delay * attempt);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new SystemException("Interrupted during retry");
                }
            }
        }
        
        throw new SystemException("Unexpected error in retry logic");
    }
}

⚠️ API Development Best Practices

  • Always validate sessions - every API call must check session validity
  • Use consistent error handling - follow the ResponseStatusHeader pattern
  • Log everything - API calls are audited for compliance
  • Handle timeouts gracefully - external services can be slow
  • Validate business rules - don't rely on client-side validation
  • Use database transactions - financial operations must be atomic