Technical Guide

How to Integrate Address APIs in Java: Verification, Autocomplete & Geocoding

A complete guide to integrating address verification, autocomplete, parsing, and geocoding APIs in Java applications using HttpClient and Spring Boot.

Sthan.io Team
Sthan.io Team
March 7, 2026 · 15 min read

Prerequisites

Before you begin, make sure you have:

  • Java 11+ (for java.net.http.HttpClient)
  • Maven or Gradle for dependency management
  • A Sthan.io account (free tier includes 100,000 API calls/month)
  • A text editor or IDE (IntelliJ IDEA or VS Code recommended)

Add Jackson Dependency

We use Jackson for JSON parsing. Add it to your pom.xml:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.0</version>
</dependency>

Or if you use Gradle:

implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
2-Step Authentication:

Sthan.io uses a 2-step authentication flow. First, you obtain a JWT token by calling GET /Auth/Token with your profileName and profilePassword headers. Then, you use that token as a Bearer token in subsequent API calls. The token is valid for a limited time, so you should cache and refresh it as needed.

Authentication & Reusable API Client

Let's build a reusable SthanApiClient class that handles authentication, token caching, and API requests using Java's built-in java.net.http.HttpClient.

Step 1: Get a JWT Token

Call GET https://api.sthan.io/Auth/Token with your profileName and profilePassword as request headers:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class SthanAuth {

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.sthan.io/Auth/Token"))
                .header("profileName", "YOUR_PROFILE_NAME")
                .header("profilePassword", "YOUR_PROFILE_PASSWORD")
                .GET()
                .build();

        HttpResponse<String> response = client.send(
                request, HttpResponse.BodyHandlers.ofString());

        System.out.println("Status: " + response.statusCode());

        ObjectMapper mapper = new ObjectMapper();
        JsonNode json = mapper.readTree(response.body());

        // Extract the token from the wrapped response
        String token = json.get("result")
                           .get("access_token").asText();

        System.out.println("Token: " + token);
    }
}

The response follows the standard Sthan.io envelope format:

{
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "result": {
        "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "expiration": "2026-03-07T18:30:00Z"
    },
    "statusCode": 200,
    "isError": false,
    "errors": []
}

Step 2: Build a Reusable Client with Token Caching

For production use, create a class that caches the JWT token and refreshes it automatically:

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class SthanApiClient {

    private static final String BASE_URL = "https://api.sthan.io";
    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;
    private final String profileName;
    private final String profilePassword;

    private String cachedToken;
    private Instant tokenExpiry;

    public SthanApiClient(String profileName, String profilePassword) {
        this.profileName = profileName;
        this.profilePassword = profilePassword;
        this.objectMapper = new ObjectMapper();
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();
    }

    /**
     * Get a valid JWT token, refreshing if expired.
     */
    public synchronized String getToken() throws Exception {
        if (cachedToken != null && tokenExpiry != null
                && Instant.now().isBefore(tokenExpiry.minusSeconds(30))) {
            return cachedToken;
        }

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE_URL + "/Auth/Token"))
                .header("profileName", profileName)
                .header("profilePassword", profilePassword)
                .GET()
                .build();

        HttpResponse<String> response = httpClient.send(
                request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException(
                "Authentication failed: HTTP " + response.statusCode());
        }

        JsonNode json = objectMapper.readTree(response.body());
        JsonNode result = json.get("result");
        cachedToken = result.get("access_token").asText();
        String expiration = result.get("expiration").asText();
        tokenExpiry = Instant.parse(expiration);

        return cachedToken;
    }

    /**
     * Make an authenticated GET request to a Sthan.io endpoint.
     */
    public JsonNode get(String path) throws Exception {
        String token = getToken();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE_URL + path))
                .header("Authorization", "Bearer " + token)
                .timeout(Duration.ofSeconds(30))
                .GET()
                .build();

        HttpResponse<String> response = httpClient.send(
                request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException(
                "API request failed: HTTP " + response.statusCode()
                + " - " + response.body());
        }

        return objectMapper.readTree(response.body());
    }

    /**
     * URL-encode a string for use in API paths.
     */
    public static String encode(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }
}
Pro Tip: The getToken() method is synchronized to ensure thread safety. The token is refreshed 30 seconds before expiry to avoid race conditions. Create a single SthanApiClient instance and reuse it across your application.

Address Autocomplete

The Address Autocomplete API returns address suggestions as the user types. Call GET /AutoComplete/USA/Address/{text} with a partial address string.

import com.fasterxml.jackson.databind.JsonNode;

public class AutocompleteExample {

    public static void main(String[] args) throws Exception {
        SthanApiClient client = new SthanApiClient(
                "YOUR_PROFILE_NAME", "YOUR_PROFILE_PASSWORD");

        String partialAddress = "1600 pennsylvania";
        String path = "/AutoComplete/USA/Address/"
                     + SthanApiClient.encode(partialAddress);

        JsonNode response = client.get(path);

        // Extract suggestions from the result
        JsonNode result = response.get("result");
        if (result != null && result.isArray()) {
            System.out.println("Address suggestions:");
            for (JsonNode suggestion : result) {
                System.out.println("  - " + suggestion.asText());
            }
        }
    }
}

Example response:

{
    "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "result": [
        "1600 Pennsylvania Ave NW, Washington, DC 20500",
        "1600 Pennsylvania Ave, Hagerstown, MD 21742",
        "1600 Pennsylvania Ave SE, Washington, DC 20003"
    ],
    "statusCode": 200,
    "isError": false,
    "errors": []
}
Performance: Address autocomplete responses typically return in sub-100ms. For UI integrations, combine with debouncing (300ms) to avoid excessive API calls while the user is still typing.

Address Verification

The Address Verification API validates and standardizes a full US address. Call GET /AddressVerification/Usa/Single/{address} to verify deliverability and get the standardized USPS format.

import com.fasterxml.jackson.databind.JsonNode;

public class VerificationExample {

    public static void main(String[] args) throws Exception {
        SthanApiClient client = new SthanApiClient(
                "YOUR_PROFILE_NAME", "YOUR_PROFILE_PASSWORD");

        String address = "1600 Pennsylvania Ave NW, Washington DC 20500";
        String path = "/AddressVerification/Usa/Single/"
                     + SthanApiClient.encode(address);

        JsonNode response = client.get(path);

        JsonNode result = response.get("result");
        if (result != null) {
            System.out.println("Verified Address:");
            System.out.println("  Street: "
                + result.path("addressLine1").asText());
            System.out.println("  City: "
                + result.path("city").asText());
            System.out.println("  State: "
                + result.path("state").asText());
            System.out.println("  ZIP: "
                + result.path("zipCode").asText());
            System.out.println("  ZIP+4: "
                + result.path("plus4Code").asText());
            System.out.println("  Deliverable: "
                + result.path("isDeliverable").asBoolean());
        }

        // Check for errors
        if (response.get("isError").asBoolean()) {
            JsonNode errors = response.get("errors");
            for (JsonNode error : errors) {
                System.err.println("Error: " + error.asText());
            }
        }
    }
}

Example response:

{
    "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
    "result": {
        "addressLine1": "1600 PENNSYLVANIA AVE NW",
        "city": "WASHINGTON",
        "state": "DC",
        "zipCode": "20500",
        "plus4Code": "0005",
        "isDeliverable": true,
        "dpvMatchCode": "Y",
        "isResidential": false
    },
    "statusCode": 200,
    "isError": false,
    "errors": []
}
Note: Address verification typically takes 1-5 seconds depending on address complexity. For batch processing, use asynchronous requests with HttpClient.sendAsync().

Address Parsing

The Address Parser API breaks a freeform address string into its individual components (street number, street name, city, state, ZIP). Call GET /AddressParser/USA/Single/{address}.

import com.fasterxml.jackson.databind.JsonNode;

public class ParserExample {

    public static void main(String[] args) throws Exception {
        SthanApiClient client = new SthanApiClient(
                "YOUR_PROFILE_NAME", "YOUR_PROFILE_PASSWORD");

        String address = "123 Main St Apt 4B, Springfield, IL 62701";
        String path = "/AddressParser/USA/Single/"
                     + SthanApiClient.encode(address);

        JsonNode response = client.get(path);

        JsonNode result = response.get("result");
        if (result != null) {
            System.out.println("Parsed Address Components:");
            System.out.println("  Street Number: "
                + result.path("streetNumber").asText());
            System.out.println("  Street Name: "
                + result.path("streetName").asText());
            System.out.println("  Street Type: "
                + result.path("streetType").asText());
            System.out.println("  Unit Type: "
                + result.path("unitType").asText());
            System.out.println("  Unit Number: "
                + result.path("unitNumber").asText());
            System.out.println("  City: "
                + result.path("city").asText());
            System.out.println("  State: "
                + result.path("state").asText());
            System.out.println("  ZIP Code: "
                + result.path("zipCode").asText());
        }
    }
}

Example response:

{
    "id": "d4e5f6a7-b8c9-0123-defa-234567890123",
    "result": {
        "streetNumber": "123",
        "streetName": "MAIN",
        "streetType": "ST",
        "unitType": "APT",
        "unitNumber": "4B",
        "city": "SPRINGFIELD",
        "state": "IL",
        "zipCode": "62701"
    },
    "statusCode": 200,
    "isError": false,
    "errors": []
}

Forward Geocoding

The Forward Geocoding API converts a US address into latitude/longitude coordinates. Call GET /Geocoding/USA/Forward/{address}.

import com.fasterxml.jackson.databind.JsonNode;

public class GeocodingExample {

    public static void main(String[] args) throws Exception {
        SthanApiClient client = new SthanApiClient(
                "YOUR_PROFILE_NAME", "YOUR_PROFILE_PASSWORD");

        String address = "1600 Amphitheatre Parkway, Mountain View, CA 94043";
        String path = "/Geocoding/USA/Forward/"
                     + SthanApiClient.encode(address);

        JsonNode response = client.get(path);

        JsonNode result = response.get("result");
        if (result != null) {
            double latitude = result.path("latitude").asDouble();
            double longitude = result.path("longitude").asDouble();

            System.out.println("Geocoding Result:");
            System.out.println("  Latitude:  " + latitude);
            System.out.println("  Longitude: " + longitude);
            System.out.println("  Formatted: "
                + result.path("formattedAddress").asText());
        }
    }
}

Example response:

{
    "id": "e5f6a7b8-c9d0-1234-efab-345678901234",
    "result": {
        "latitude": 37.4220656,
        "longitude": -122.0862784,
        "formattedAddress": "1600 AMPHITHEATRE PKWY, MOUNTAIN VIEW, CA 94043"
    },
    "statusCode": 200,
    "isError": false,
    "errors": []
}

Spring Boot Integration

For production Spring Boot applications, externalize credentials with @ConfigurationProperties, create a @Service bean wrapping the API, and expose endpoints through a @RestController.

Step 1: Configuration Properties

Add your API credentials to application.yml:

# application.yml
sthan:
  api:
    base-url: https://api.sthan.io
    profile-name: YOUR_PROFILE_NAME
    profile-password: YOUR_PROFILE_PASSWORD

Create the configuration properties class:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sthan.api")
public class SthanApiProperties {

    private String baseUrl;
    private String profileName;
    private String profilePassword;

    // Getters and setters
    public String getBaseUrl() { return baseUrl; }
    public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }

    public String getProfileName() { return profileName; }
    public void setProfileName(String profileName) {
        this.profileName = profileName;
    }

    public String getProfilePassword() { return profilePassword; }
    public void setProfilePassword(String profilePassword) {
        this.profilePassword = profilePassword;
    }
}

Step 2: Service Class

Create a @Service that wraps all address API calls using Spring's RestTemplate:

import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import java.time.Instant;

@Service
public class SthanAddressService {

    private final SthanApiProperties properties;
    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;

    private String cachedToken;
    private Instant tokenExpiry;

    public SthanAddressService(SthanApiProperties properties) {
        this.properties = properties;
        this.restTemplate = new RestTemplate();
        this.objectMapper = new ObjectMapper();
    }

    /**
     * Get or refresh the JWT token.
     */
    private synchronized String getToken() {
        if (cachedToken != null && tokenExpiry != null
                && Instant.now().isBefore(tokenExpiry.minusSeconds(30))) {
            return cachedToken;
        }

        HttpHeaders headers = new HttpHeaders();
        headers.set("profileName", properties.getProfileName());
        headers.set("profilePassword", properties.getProfilePassword());

        ResponseEntity<String> response = restTemplate.exchange(
                properties.getBaseUrl() + "/Auth/Token",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class);

        try {
            JsonNode json = objectMapper.readTree(response.getBody());
            JsonNode result = json.get("result");
            cachedToken = result.get("access_token").asText();
            tokenExpiry = Instant.parse(
                    result.get("expiration").asText());
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse auth token", e);
        }

        return cachedToken;
    }

    /**
     * Make an authenticated GET request.
     */
    private JsonNode apiGet(String path) {
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(getToken());

        ResponseEntity<String> response = restTemplate.exchange(
                properties.getBaseUrl() + path,
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class);

        try {
            return objectMapper.readTree(response.getBody());
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse response", e);
        }
    }

    private String encode(String value) {
        return UriUtils.encodePath(value, StandardCharsets.UTF_8);
    }

    /** Address autocomplete. */
    public JsonNode autocomplete(String query) {
        return apiGet("/AutoComplete/USA/Address/" + encode(query));
    }

    /** Address verification. */
    public JsonNode verify(String address) {
        return apiGet("/AddressVerification/Usa/Single/"
                     + encode(address));
    }

    /** Address parsing. */
    public JsonNode parse(String address) {
        return apiGet("/AddressParser/USA/Single/" + encode(address));
    }

    /** Forward geocoding. */
    public JsonNode geocode(String address) {
        return apiGet("/Geocoding/USA/Forward/" + encode(address));
    }
}

Step 3: REST Controller

Expose the address services through a @RestController so your frontend or other services can consume them:

import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.JsonNode;

@RestController
@RequestMapping("/api/address")
public class AddressController {

    private final SthanAddressService addressService;

    public AddressController(SthanAddressService addressService) {
        this.addressService = addressService;
    }

    @GetMapping("/autocomplete")
    public JsonNode autocomplete(
            @RequestParam String query) {
        return addressService.autocomplete(query);
    }

    @GetMapping("/verify")
    public JsonNode verify(
            @RequestParam String address) {
        return addressService.verify(address);
    }

    @GetMapping("/parse")
    public JsonNode parse(
            @RequestParam String address) {
        return addressService.parse(address);
    }

    @GetMapping("/geocode")
    public JsonNode geocode(
            @RequestParam String address) {
        return addressService.geocode(address);
    }
}
Spring Boot Tip: The @Service bean is a singleton by default, so the token cache is shared across all requests. The synchronized keyword on getToken() ensures thread-safe token refresh under concurrent load. For reactive Spring WebFlux applications, replace RestTemplate with WebClient.

Error Handling & Best Practices

Custom Exception Class

Create a dedicated exception for API errors:

public class SthanApiException extends RuntimeException {

    private final int statusCode;
    private final String responseBody;

    public SthanApiException(int statusCode, String responseBody) {
        super("Sthan API error: HTTP " + statusCode);
        this.statusCode = statusCode;
        this.responseBody = responseBody;
    }

    public int getStatusCode() { return statusCode; }
    public String getResponseBody() { return responseBody; }
}

Retry Logic with Exponential Backoff

Implement retry logic for transient failures (network errors, 429 rate limiting, 5xx server errors):

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class RetryHelper {

    private static final int MAX_RETRIES = 3;
    private static final long BASE_DELAY_MS = 1000;

    /**
     * Execute an HTTP request with exponential backoff retry.
     */
    public static HttpResponse<String> executeWithRetry(
            HttpClient client, HttpRequest request) throws Exception {

        Exception lastException = null;

        for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) {
            try {
                HttpResponse<String> response = client.send(
                        request, HttpResponse.BodyHandlers.ofString());

                int status = response.statusCode();

                // Success or client error (don't retry 4xx except 429)
                if (status < 500 && status != 429) {
                    return response;
                }

                // Rate limited or server error -- retry
                if (attempt < MAX_RETRIES) {
                    long delay = BASE_DELAY_MS * (long) Math.pow(2, attempt);
                    // Add jitter to prevent thundering herd
                    delay += (long) (Math.random() * delay * 0.1);
                    Thread.sleep(delay);
                }

            } catch (java.io.IOException e) {
                lastException = e;
                if (attempt < MAX_RETRIES) {
                    long delay = BASE_DELAY_MS * (long) Math.pow(2, attempt);
                    Thread.sleep(delay);
                }
            }
        }

        throw new RuntimeException(
            "Request failed after " + MAX_RETRIES + " retries",
            lastException);
    }
}

URL Encoding

Always URL-encode address strings before including them in the request path. Characters like #, &, and spaces must be encoded:

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

// Always encode addresses in the URL path
String address = "123 Main St #4B, Springfield, IL";
String encoded = URLEncoder.encode(address, StandardCharsets.UTF_8);
String path = "/AddressVerification/Usa/Single/" + encoded;

Production Best Practices

  • Connection pooling: java.net.http.HttpClient manages a connection pool internally. Create one instance and reuse it across your application.
  • Timeouts: Always set both connectTimeout (on the client) and timeout (on individual requests). Use 10s for connect and 30s for read.
  • Thread safety: HttpClient and ObjectMapper are thread-safe. Use synchronized or AtomicReference for token caching.
  • Token refresh: Cache the JWT token and refresh it 30 seconds before expiry. Never request a new token for every API call.
  • Rate limiting: Respect API rate limits. If you receive HTTP 429, back off and retry with exponential delay. Use a Semaphore to limit concurrent requests.
  • Logging: Log request/response details at DEBUG level and errors at ERROR level. Never log the full JWT token or credentials in production.
  • Batch processing: For bulk address processing, use HttpClient.sendAsync() with CompletableFuture to make concurrent requests, controlled by a semaphore.
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;

public class BatchProcessor {

    // Batch processing with controlled concurrency
    public List<JsonNode> verifyBatch(
        SthanApiClient client,
        List<String> addresses,
        int maxConcurrency) throws Exception {

    Semaphore semaphore = new Semaphore(maxConcurrency);

    List<CompletableFuture<JsonNode>> futures = addresses.stream()
            .map(address -> CompletableFuture.supplyAsync(() -> {
                try {
                    semaphore.acquire();
                    try {
                        return client.get(
                            "/AddressVerification/Usa/Single/"
                            + SthanApiClient.encode(address));
                    } finally {
                        semaphore.release();
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }))
            .collect(Collectors.toList());

    return futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
    }
}

Key Takeaways

  • Use Java 11+ java.net.http.HttpClient for a zero-dependency HTTP client with built-in connection pooling
  • Cache JWT tokens and refresh them before expiry -- never request a new token per API call
  • Always URL-encode addresses with URLEncoder.encode(address, StandardCharsets.UTF_8)
  • Implement retry logic with exponential backoff for transient failures and rate limiting
  • For Spring Boot, use @Service + @ConfigurationProperties to externalize credentials and create reusable service beans
  • Control batch concurrency with a Semaphore to respect API rate limits

Share This Article

Frequently Asked Questions

For Java 11 and above, we recommend using the built-in java.net.http.HttpClient. It supports asynchronous requests, HTTP/2, and requires no external dependencies. For Spring Boot applications, use RestTemplate for synchronous calls or WebClient for reactive/asynchronous calls. Apache HttpClient is another mature option if you need fine-grained connection pool control.

Implement exponential backoff with jitter when you receive HTTP 429 (Too Many Requests) responses. Cache authentication tokens to avoid unnecessary token requests. Use a Semaphore or rate limiter library like Bucket4j to throttle outgoing requests on your side. For batch processing, add delays between requests and process addresses in controlled batches rather than all at once. Check our API documentation for specific rate limit details.

Yes, Sthan.io APIs integrate seamlessly with Spring Boot. You can create a service class annotated with @Service that wraps the API calls, use @ConfigurationProperties to externalize your API credentials, and inject the service into your controllers. See the Spring Boot Integration section above for a complete working example with RestTemplate, configuration properties, and a REST controller.

The examples in this guide require Java 11 or higher because they use java.net.http.HttpClient, which was introduced in Java 11. The Spring Boot examples work with Spring Boot 2.x (Java 8+) or Spring Boot 3.x (Java 17+). If you are on Java 8, you can use Apache HttpClient or OkHttp as drop-in alternatives to java.net.http.HttpClient.

For bulk processing, use Java's CompletableFuture with HttpClient.sendAsync() to make concurrent requests. Control concurrency with a Semaphore to respect API rate limits. Process addresses in batches of 10-50, add a small delay between batches, and implement retry logic for failed requests. Cache your JWT token and refresh it only when it expires to minimize authentication overhead. See the batch processing example above.

Use Jackson (com.fasterxml.jackson.databind) to parse JSON responses. Create Java POJO classes that map to the API response structure, including the outer wrapper with id, result, statusCode, isError, and errors fields. Use ObjectMapper to deserialize the JSON string into your Java objects. Alternatively, use JsonNode for dynamic parsing without creating POJOs, as shown in the examples above.

Yes, java.net.http.HttpClient is thread-safe and designed to be shared across threads. The SthanApiClient class shown in this guide uses a single HttpClient instance and can be safely used from multiple threads. For token caching, use synchronized blocks (as shown) or AtomicReference to ensure thread-safe token refresh. In Spring Boot, the service bean is a singleton by default, which works well with the thread-safe HttpClient.

Start Building with Sthan.io

Get 100,000 free API calls per month. Address verification, autocomplete, parsing, and geocoding -- all in one platform. No credit card required.

Sthan.io Team
Written by Sthan.io Team

The Sthan.io engineering team builds and maintains address verification, parsing, geocoding, and autocomplete APIs processing millions of requests daily. With deep expertise in USPS postal standards, spatial data systems, and high-throughput API infrastructure, we help 2,000+ businesses improve address data quality and reduce failed deliveries.

Learn more about us