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 (Liststreet (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¶
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¶
Purpose: Batch framework method that processes each batch of records by geocoding their addresses.
Parameters:
- bc (Database.BatchableContext): Batch context (not used)
- scope (List
Business Logic: - Delegates to updateGeolocations() method - Passes scope and field mappings - Processes up to batch size records per execution
finish¶
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
Business Logic:
- Validation:
-
Returns early if records null or empty
-
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); - Concatenates address fields with spaces
- Replaces spaces with '+' for URL encoding
-
Removes 'null+' strings from null field values
-
Response Parsing:
-
Record Update Preparation:
- Creates new SObject instance dynamically
- If status == 'OK': Sets latitude and longitude from response
- If status != 'OK': Sets latitude and longitude to null
-
Adds to recordsToUpdate list
-
Bulk Update:
- Uses Database.update with allOrNone=false
- 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:
- Validation:
-
Returns null if any parameter is blank
-
Configuration:
-
Retrieves endpoint and API key from Custom Labels
-
HTTP Request:
-
Callout:
- In production: Makes actual HTTP callout
- In test: Returns mock response from Static Resource
API Format:
getSObjectName¶
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¶
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¶
Geometry¶
Location¶
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 URLG_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)