Skip to content

Class Name: GeolocationServiceBatchable

Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/GeolocationServiceBatchable.cls

API Name: GeolocationServiceBatchable Type: Batch Apex (with HTTP Callouts) Test Coverage: GeolocationServiceTest.cls Author: Unknown Created: Unknown

Business Purpose

This batch class geocodes address data by calling the Google Maps Geocoding API to populate latitude and longitude coordinates on Salesforce records. It enables location-based functionality such as territory management, distance calculations, "find nearest" features, and mapping capabilities. The class is designed to handle large datasets efficiently through Salesforce's batch framework while making external HTTP callouts to convert physical addresses into geographic coordinates.

Class Overview

Scope and Sharing

  • Sharing Model: with sharing (respects record-level security)
  • Access Modifier: public
  • Interfaces Implemented:
  • Database.Batchable
  • Database.AllowsCallouts

Key Responsibilities

  • Batch process records containing address information
  • Construct address strings from component fields (street, city, state, country)
  • Make HTTP callouts to Google Maps Geocoding API
  • Parse JSON responses from geocoding service
  • Update records with latitude/longitude coordinates
  • Handle geocoding failures by setting coordinates to null
  • Support any SObject type through dynamic field mapping
  • Work within Salesforce governor limits (100 callouts per execution)

Public Methods

Constructor

public GeolocationServiceBatchable(
    List<SObject> records,
    final Schema.SObjectField street,
    final Schema.SObjectField city,
    final Schema.SObjectField state,
    final Schema.SObjectField country,
    final Schema.SObjectField latitude,
    final Schema.SObjectField longitude
)

Purpose: Initializes the batch job with records to geocode and field mappings for address components and coordinate storage.

Parameters: - records (List): Records containing addresses to geocode - street (Schema.SObjectField): Field containing street address - city (Schema.SObjectField): Field containing city - state (Schema.SObjectField): Field containing state/province - country (Schema.SObjectField): Field containing country - latitude (Schema.SObjectField): Field to store latitude coordinate - longitude (Schema.SObjectField): Field to store longitude coordinate

Usage Example:

List<Order> orders = [SELECT Id, BillingStreet, BillingCity, BillingState, BillingCountry
                      FROM Order WHERE BillingLatitude = null LIMIT 100];

GeolocationServiceBatchable batch = new GeolocationServiceBatchable(
    orders,
    Schema.Order.BillingStreet,
    Schema.Order.BillingCity,
    Schema.Order.BillingState,
    Schema.Order.BillingCountry,
    Schema.Order.BillingLatitude,
    Schema.Order.BillingLongitude
);

Database.executeBatch(batch, 100);


start

public System.Iterable<SObject> start(Database.BatchableContext bc)

Purpose: Batch framework method that returns the collection of records to process.

Parameters: - bc (Database.BatchableContext): Batch context (not used in implementation)

Returns: - System.Iterable<SObject>: CustomIterable wrapper around records list

Business Logic: - Returns CustomIterable instead of standard Iterable - Wraps this.records in custom iterator - Allows controlled iteration over records


execute

public void execute(Database.BatchableContext bc, List<SObject> scope)

Purpose: Batch framework method that processes each batch of records by geocoding their addresses.

Parameters: - bc (Database.BatchableContext): Batch context (not used) - scope (List): Batch of records to process

Business Logic: - Delegates to updateGeolocations() method - Passes scope and field mappings - Processes up to batch size records per execution


finish

public void finish(Database.BatchableContext bc)

Purpose: Batch framework method called after all batches complete.

Parameters: - bc (Database.BatchableContext): Batch context

Business Logic: - Empty implementation (no cleanup needed) - Could be used for completion notifications or logging


updateGeolocations

public void updateGeolocations(
    List<SObject> records,
    final Schema.SObjectField street,
    final Schema.SObjectField city,
    final Schema.SObjectField state,
    final Schema.SObjectField country,
    final Schema.SObjectField latitude,
    final Schema.SObjectField longitude
)

Purpose: Core method that geocodes addresses and updates records with coordinates.

Parameters: - records (List): Records to geocode - Field parameters: Same as constructor

Business Logic:

  1. Validation:
  2. Returns early if records null or empty

  3. Address Construction & Geocoding:

    for(SObject record : records) {
        String address = String.valueOf(
            record.get(street) + ' ' +
            record.get(city) + ' ' +
            record.get(state) + ' ' +
            record.get(country)
        ).replace(' ', '+');
        address = address.replace('null+','');  // Remove null values
    
        res = getGeolocation(address, record.Id, latitude, longitude);
    

  4. Concatenates address fields with spaces
  5. Replaces spaces with '+' for URL encoding
  6. Removes 'null+' strings from null field values

  7. Response Parsing:

    AddressResponse addressResponse = (AddressResponse) JSON.deserialize(
        res.getBody(),
        AddressResponse.class
    );
    

  8. Record Update Preparation:

  9. Creates new SObject instance dynamically
  10. If status == 'OK': Sets latitude and longitude from response
  11. If status != 'OK': Sets latitude and longitude to null
  12. Adds to recordsToUpdate list

  13. Bulk Update:

    if(!this.recordsToUpdate.isEmpty()) {
        Database.update(this.recordsToUpdate, false);
    }
    

  14. Uses Database.update with allOrNone=false
  15. Allows partial success

getGeolocation

public HTTPResponse getGeolocation(
    final String address,
    String recordId,
    final String latitude,
    final String longitude
)

Purpose: Makes HTTP callout to Google Maps Geocoding API.

Parameters: - address (String): URL-encoded address string - recordId (String): Record ID (for logging, not used) - latitude (String): Latitude field name (not used) - longitude (String): Longitude field name (not used)

Returns: - HTTPResponse: Response from Google Maps API

Business Logic:

  1. Validation:
  2. Returns null if any parameter is blank

  3. Configuration:

    final String ENDPOINT = System.Label.G_Maps_Get_Geolocation_Endpoint;
    final String G_Maps_Key = System.Label.G_Maps_Key;
    

  4. Retrieves endpoint and API key from Custom Labels

  5. HTTP Request:

    HttpRequest req = new HttpRequest();
    req.setEndpoint(ENDPOINT + '?address=' + address + '&key=' + G_Maps_Key);
    req.setMethod('GET');
    

  6. Callout:

  7. In production: Makes actual HTTP callout
  8. In test: Returns mock response from Static Resource

API Format:

GET https://maps.googleapis.com/maps/api/geocode/json?address={address}&key={api_key}


getSObjectName

private String getSObjectName(String recordId)

Purpose: Helper method to extract SObject type from record ID.

Parameters: - recordId (String): Salesforce record ID

Returns: - String: SObject API name

Business Logic: - Uses Id.getSObjectType().getDescribe().getName() - Enables dynamic SObject instantiation


Inner Classes

AddressResponse

public class AddressResponse {
    public String status;
    public List<Result> results;
}

Purpose: Represents Google Maps API JSON response structure.

Fields: - status: Response status ('OK', 'ZERO_RESULTS', 'OVER_QUERY_LIMIT', etc.) - results: List of geocoding results

Result

public class Result {
    public Geometry geometry;
}

Geometry

public class Geometry {
    public Location location;
}

Location

public class Location {
    public Decimal lat;
    public Decimal lng;
}

JSON Structure:

{
  "status": "OK",
  "results": [{
    "geometry": {
      "location": {
        "lat": 37.4224764,
        "lng": -122.0842499
      }
    }
  }]
}


CustomIterable

Purpose: Custom iterator for batch processing (implementation not shown in source).


Dependencies

Apex Classes

  • None - Standalone batch class
  • Called by: OrderAddressPopulation, AccountTriggerHandler, or similar

Salesforce Objects

Any SObject with address fields, commonly: - Order (BillingStreet, BillingCity, BillingLatitude, etc.) - Account (BillingStreet, ShippingStreet, etc.) - Contact (MailingStreet, MailingCity, etc.) - Custom objects with address fields

Custom Settings/Metadata

  • Custom Labels (REQUIRED):
  • G_Maps_Get_Geolocation_Endpoint: Google Maps API endpoint URL
  • G_Maps_Key: Google Maps API key

External Services

  • Google Maps Geocoding API
  • Endpoint: https://maps.googleapis.com/maps/api/geocode/json
  • Authentication: API key
  • Rate limits: Based on billing plan
  • Requires Remote Site Setting

Static Resources

  • AddressResponse: Mock JSON response for testing

Design Patterns

  • Batch Pattern: Implements Database.Batchable for large data processing
  • Strategy Pattern: Flexible field mapping for different SObjects
  • DTO Pattern: AddressResponse classes map JSON structure
  • Template Method Pattern: Standard batch lifecycle (start/execute/finish)

Why These Patterns: - Batch pattern handles large datasets within governor limits - Strategy pattern allows reuse across different SObjects - DTO pattern simplifies JSON parsing - Template method provides consistent framework

Governor Limits Considerations

SOQL Queries: 0 (records provided in constructor) DML Operations: 1 per execute (Database.update) HTTP Callouts: Up to 100 per execute (1 per record in scope) CPU Time: Medium (JSON parsing, string manipulation) Heap Size: Proportional to batch size

Bulkification: Partial - DML is bulkified (all records updated at once) - Callouts are NOT bulkified (1 per record, sequential)

Async Processing: Yes (batch framework)

Governor Limit Risks: - CRITICAL: 100 callout limit per execution - batch size must be ≤100 - HIGH: Sequential callouts are slow - large batches take minutes - MEDIUM: Heap size with large batches and JSON responses - LOW: DML row limits (10,000 per transaction)

Callout Limit Math: - Batch size 100 = 100 callouts (at limit) - Batch size 200 = FAILS (exceeds 100 callout limit) - Recommended: Batch size 50-75 for safety margin

Recommendations: - CRITICAL: Document that batch size must be ≤100 - Add validation in constructor to enforce max batch size - Consider async queueable chaining for >100 callouts - Monitor API quota usage

Error Handling

Strategy: Minimal error handling - relies on partial success

Logging: - Debug statement for addressResponse.status - No logging of failures or exceptions - No persistent error tracking

Callout Errors: - Not explicitly handled - System exceptions propagate to batch framework - Could fail entire batch execution

DML Errors: - Uses Database.update with allOrNone=false - Allows partial success - Doesn't log which records failed

API Response Handling: - Sets coordinates to null if status != 'OK' - Doesn't log failed geocoding attempts - No retry for failed addresses

Recommended Improvements: - Add try-catch around each callout - Log failed records to custom object - Implement retry logic for transient failures - Track API quota usage - Email notifications on batch failures

Security Considerations

Sharing Rules: RESPECTED - Uses 'with sharing' - Only processes records user has access to - Appropriate for batch processing

Field-Level Security: RESPECTED - FLS enforced on field access - User must have edit access to coordinate fields

CRUD Permissions: RESPECTED - User must have edit permission on object

API Key Security: ⚠️ EXPOSED - API key stored in Custom Label - Visible to users with "View Setup" or "Customize Application" - RISK: Key could be extracted and misused

Input Validation: MINIMAL - No validation of address data quality - No sanitization of address strings - Trusts field values

External Service Security: - HTTPS required for API endpoint - Remote Site Setting required - No authentication beyond API key

Mitigation Recommendations: 1. CRITICAL: Move API key to Protected Custom Metadata or Named Credential 2. Validate addresses before geocoding 3. Implement rate limiting to prevent abuse 4. Monitor API usage for anomalies 5. Restrict batch execution permissions

Test Class

Test Class: GeolocationServiceTest.cls Coverage: To be determined

Test Scenarios That Should Be Covered: - ✓ Successful geocoding with valid address - ✓ Failed geocoding (status != 'OK') - ✓ Multiple records in batch - ✓ Null address fields handled - ✓ Empty address strings - ✓ API callout mock using Static Resource - ✓ DML partial success scenarios - ✓ Different SObject types (Order, Account, Contact) - ✓ Batch size limits - ✓ CustomIterable functionality

Testing Challenges: - Must mock HTTP callouts using Test.setMock() - Requires Static Resource 'AddressResponse' - Multiple SObject types need separate test methods - Callout limits difficult to test

Test Data Requirements: - Records with complete address data - Records with partial address data - Static Resource with mock Google Maps JSON response

Changes & History

  • Created: Unknown (check git history)
  • Author: Unknown
  • Purpose: Geocode addresses via Google Maps API
  • Related to: Location-based features, territory management

⚠️ Pre-Go-Live Concerns

CRITICAL - Fix Before Go-Live

  • API Key Security: Google Maps API key in Custom Label is visible to many users. Move to Protected Custom Metadata or Named Credential immediately.
  • Callout Limit: Batch size NOT validated against 100-callout limit. Will FAIL if batch size >100. Add validation in constructor.
  • No Error Handling on Callouts: HTTP callout exceptions will crash entire batch. Add try-catch around each callout.
  • Cost Management: No controls on API usage - could incur significant costs. Implement quota monitoring and alerts.

HIGH - Address Soon After Go-Live

  • No Retry Logic: Transient failures (network issues, API throttling) result in permanent null coordinates. Implement retry mechanism.
  • Sequential Callouts: Processing 100 records takes ~200 seconds (2 seconds per callout). Extremely slow. Consider async alternatives.
  • No Duplicate Prevention: Re-geocodes addresses even if coordinates already exist. Wastes API quota.
  • Missing Logging: No record of failed geocoding attempts. Impossible to troubleshoot or track data quality issues.

MEDIUM - Future Enhancement

  • Address Validation: No pre-validation of address completeness or format. Wasted callouts on invalid addresses.
  • Hardcoded Static Resource: 'AddressResponse' name hardcoded for tests. Should be configurable.
  • Limited Status Handling: Only checks 'OK' status. Doesn't differentiate between ZERO_RESULTS, OVER_QUERY_LIMIT, INVALID_REQUEST, etc.
  • No Monitoring Dashboard: Cannot track geocoding success rates, API usage, or costs.

LOW - Monitor

  • Custom Iterator Complexity: CustomIterable may be unnecessary - standard List works fine.
  • Unused Parameters: recordId, latitude, longitude parameters in getGeolocation() not used.
  • Missing Documentation: No JavaDoc comments explaining usage or limitations.
  • API Version: Should verify compatibility with current Google Maps API version.

Maintenance Notes

Complexity: High (external API integration + batch processing) Recommended Review Schedule: Monthly (API costs), Quarterly (functionality)

Key Maintainer Notes:

🌍 GOOGLE MAPS API DEPENDENCY: - This class is ENTIRELY dependent on Google Maps API - Requires active billing account with Google - API changes or outages break functionality - Monitor API usage and costs monthly - Review Google's terms of service compliance

📋 Usage Patterns: - Called from triggers (OrderAddressPopulation, etc.) - Runs asynchronously via Database.executeBatch() - Typical batch size: 100 records - Execution time: ~2 seconds per record (200 seconds for 100 records)

🧪 Testing Requirements: - Must use Test.setMock() for HTTP callouts - Requires 'AddressResponse' Static Resource - Test with various address formats - Test batch size limits - Verify partial success handling

🔧 API Configuration: - Custom Labels Required: - G_Maps_Get_Geolocation_Endpoint - G_Maps_Key - Remote Site Setting Required: - https://maps.googleapis.com - Static Resource Required (test only): - AddressResponse

⚠️ Gotchas and Warnings: - CRITICAL: Batch size >100 will FAIL due to callout limits - Sequential callouts are SLOW - budget 2+ seconds per record - API key in Custom Labels is NOT secure - users can extract it - "null+" in addresses comes from null field values - handle gracefully - allOrNone=false means some records may not update - Google Maps has daily quotas - can hit limits with large datasets

📅 When to Review This Class: - Monthly: Check API usage and costs - When adding new address fields - If geocoding failures increase - Before processing large datasets - After Google Maps API changes - When security audits performed

🛑 Emergency Deactivation:

If geocoding must be temporarily disabled:

// Option 1: Delete scheduled/queued batch jobs
List<AsyncApexJob> jobs = [SELECT Id FROM AsyncApexJob
    WHERE ApexClass.Name = 'GeolocationServiceBatchable'
    AND Status IN ('Queued', 'Preparing')];
// Manually abort via UI

// Option 2: Add metadata check in constructor:
Geolocation_Settings__mdt settings = Geolocation_Settings__mdt.getInstance('Main');
if (settings != null && !settings.Enabled__c) {
    throw new GeolocationException('Geocoding is disabled');
}

🔍 Debugging Tips: - Enable debug logs for batch execution - Check System Log for addressResponse.status values - Query Google Maps API directly to verify responses - Check Remote Site Settings are active - Verify API key is valid and has quota - Monitor Apex Jobs for batch status - Check Custom Labels for correct values

📊 Monitoring Checklist: - Daily: Check for failed batch jobs - Weekly: Review geocoding success rate - Monthly: Monitor API usage and costs against budget - Monthly: Review failed addresses and data quality - Quarterly: Verify API key security - Alert: Batch failures or API quota exceeded

🔗 Related Components: - OrderAddressPopulation: Calls this batch for orders - AccountTriggerHandler: May call for accounts - Google Maps API: External dependency - Custom Labels: Configuration storage - Remote Site Settings: Enable callouts - Static Resource: Test data

Business Owner

Primary: IT Operations / Data Management Team Secondary: Location Services / Territory Management Stakeholders: Sales Operations, Customer Service, Development Team, Finance (API costs)