This document presents a robust backend architecture for location tracking systems that handles high-volume geospatial data with efficient storage, real-time processing, and comprehensive privacy controls for modern mobile and IoT applications.
1. Problem Statement & Requirements
Clarifying questions
-
What scale expectations are there for the location tracking system?
- How many concurrent users/vehicles are expected to generate location updates?
- What's the target volume of location data points per second (e.g., 10k, 100k, 1M+)?
-
What consistency requirements are there for location data?
- Is real-time accuracy critical (e.g., sub-second latency), or can eventual consistency be acceptable?
- Are there any SLA requirements for data freshness?
-
What entities and their relationships are key in this system?
- Is the system tracking vehicles, users, or both?
- Are there any associated entities like routes, trips, or geofences?
-
What's the read/write ratio for location data?
- Is it primarily write-heavy (e.g., frequent GPS updates) or read-heavy (e.g., historical tracking queries)?
- Are there any batch read operations for analytics or reporting?
-
What are the latency sensitivity and performance targets?
- What is the maximum acceptable latency for a location update to be processed and made available?
- Are there any real-time dashboards or alerts that depend on location data?
-
What authentication and authorization model is expected?
- Will the system support OAuth2, JWT tokens, or API keys?
- Are there role-based access controls (RBAC) for different user types?
-
Are there any external integrations or dependencies?
- Is the system expected to integrate with mapping services (e.g., Google Maps, Mapbox)?
- Will it support real-time geofence notifications or third-party event publishers?
Functional requirements
- Real-time location tracking: Accept and store location updates from mobile devices or vehicles.
- Historical location retrieval: Retrieve past location data for a specific entity (e.g., vehicle or user) within a time range.
- Geofence monitoring: Notify when an entity enters or exits a predefined geofenced area.
- Trip analysis: Identify and analyze trips (start/end points, duration, distance).
- Location data aggregation: Aggregate location data for analytics or reporting (e.g., average speed, stops).
- Location-based search: Find entities near a given geographic coordinate (e.g., find nearby vehicles).
Non-functional requirements
-
Latency targets:
- p50: < 100ms for location update processing
- p95: < 500ms
- p99: < 1s
-
Throughput:
- Handle at least 50,000 location updates per second (with burst up to 100k/sec)
- Support batch retrieval of historical data for up to 10,000 entities
-
Availability:
- 99.9% uptime (43.2 minutes of downtime/month)
- Support for geo-redundant deployment
-
Scalability:
- Horizontally scalable to support millions of tracked entities
- Efficient storage and retrieval of location data
Constraints and assumptions
-
Storage constraints:
- Location data is stored in a time-series database or optimized relational structure.
- Data retention policy (e.g., 30 days for raw location points).
-
Infrastructure assumptions:
- The system assumes a robust, low-latency network for location updates.
- The system is designed to handle high-frequency updates (e.g., every 5 seconds).
-
Data model assumptions:
- Each tracked entity (e.g., vehicle or user) has a unique identifier.
- Location points include timestamp, latitude, longitude, and optional metadata (e.g., speed, heading).
Out of scope
-
Real-time map rendering or visualization:
- UI/UX layer and real-time map rendering are out of scope.
-
Advanced analytics or machine learning models:
- Predictive analytics, anomaly detection, or route optimization are not part of this design.
-
Third-party mapping service integration:
- Integration with external mapping services (e.g., Google Maps) is not included.
-
Mobile device SDK or client libraries:
- The design does not include client-side SDKs or mobile app integrations.
-
Data deduplication or filtering logic:
- Filtering or deduplicating location points based on signal strength or accuracy is not part of this scope.
2. API Design
API Style Selection
gRPC with Protocol Buffers is selected for the location tracking system due to:
- Efficient binary serialization - reduces payload size and network overhead
- Strong typing - compile-time validation of contracts between client and server
- Bidirectional streaming support - enables real-time location updates with minimal latency
- Built-in compression and HTTP/2 - optimal for mobile client use cases with intermittent connectivity
- Schema evolution support - versioning through field numbers and optional fields
API Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| POST | /v1/locations | Report current location for a device | JWT + Device Auth | 100 req/min |
| GET | /v1/locations/{deviceId} | Get latest location for device | JWT + Device Auth | 50 req/min |
| GET | /v1/locations/{deviceId}/history | Get location history for device | JWT + Device Auth | 20 req/min |
| POST | /v1/locations/stream | Subscribe to real-time location updates | JWT + Device Auth | 5 req/min |
| DELETE | /v1/locations/{deviceId} | Delete all location data for device | JWT + Device Auth | 5 req/min |
Request and Response Schemas
Location Report Request (gRPC)
message LocationReportRequest {
string device_id = 1;
double latitude = 2;
double longitude = 3;
int64 timestamp_ms = 4;
optional double accuracy_meters = 5;
optional double altitude_meters = 6;
optional double speed_mps = 7;
optional double heading_degrees = 8;
}
message LocationReportResponse {
bool success = 1;
string message = 2;
}
Location History Response (gRPC)
message LocationHistoryRequest {
string device_id = 1;
int64 start_timestamp_ms = 2;
int64 end_timestamp_ms = 3;
optional int32 limit = 4;
}
message LocationRecord {
string device_id = 1;
double latitude = 2;
double longitude = 3;
int64 timestamp_ms = 4;
optional double accuracy_meters = 5;
optional double altitude_meters = 6;
optional double speed_mps = 7;
optional double heading_degrees = 8;
}
message LocationHistoryResponse {
repeated LocationRecord locations = 1;
bool has_more = 2;
}
HTTP Status Codes and Error Response Format
| Status Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request - Invalid input |
| 401 | Unauthorized - Invalid auth token |
| 403 | Forbidden - Insufficient permissions |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error |
Standardized error response format:
{
"error": {
"code": "INVALID_ARGUMENT",
"message": "Latitude must be between -90 and 90",
"details": [
{
"field": "latitude",
"reason": "out_of_range"
}
]
}
}
API Versioning Strategy
Versioned using path-based versioning (/v1/locations) to:
- Allow independent evolution of API contracts
- Support multiple concurrent versions for backward compatibility
- Enable clear migration paths for clients
Pagination Design
Cursor-based pagination is used because:
- Consistent results - no issues with data changes during pagination
- Better performance - O(1) lookup for cursor-based queries
- Supports real-time data - handles concurrent writes without gaps
Sample cursor format:
{
"cursor": "eyJkZXZpY2VfaWQiOiJkaXNwbGF5X2RldmljZV9pZCIsInRpbWVzdGFtcF9tcyI6MTY4MDAwMDAwMH0="
}
Authentication and Authorization
Authentication: JWT tokens with device ID in claims
Authorization:
- Device ownership verification via device_id claim
- Device registration validation (device must be registered to user)
- Rate limiting per device_id
Rate Limiting Strategy
Per-device rate limits to prevent abuse:
- Location reporting: 100 requests/minute per device
- History queries: 20 requests/minute per device
- Stream subscriptions: 5 concurrent streams per device
Rate limiting enforced via:
- In-memory token bucket per device
- Redis distributed rate limiting for multi-instance deployments
- Sliding window algorithm to prevent burst attacks
Idempotency Design
Idempotency key strategy:
- Client provides
X-Idempotency-Keyheader with each location report request - Server stores the operation result for that key within 5 minutes
- If same key is used, return cached success/failure instead of reprocessing
// Example usage in gRPC service
public LocationReportResponse reportLocation(LocationReportRequest request) {
String idempotencyKey = getHeader("X-Idempotency-Key");
if (idempotencyKey != null && cache.has(idempotencyKey)) {
return cache.get(idempotencyKey);
}
// Process location report...
LocationReportResponse response = processLocation(request);
if (idempotencyKey != null) {
cache.put(idempotencyKey, response, Duration.ofMinutes(5));
}
return response;
}
3. Data Model Design
Entity-Relationship Diagram
erDiagram
USER {
string id PK
string email
string name
timestamp created_at
timestamp updated_at
}
LOCATION {
string id PK
string user_id FK
float latitude
float longitude
timestamp recorded_at
timestamp created_at
timestamp updated_at
}
LOCATION_HISTORY {
string id PK
string location_id FK
float latitude
float longitude
timestamp recorded_at
timestamp created_at
}
USER ||--o{ LOCATION : "records"
LOCATION ||--o{ LOCATION_HISTORY : "historical"
Table Schema Definitions
users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_users_email (email),
INDEX idx_users_created_at (created_at)
);
locations table
CREATE TABLE locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
latitude FLOAT NOT NULL,
longitude FLOAT NOT NULL,
recorded_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_locations_user_id (user_id),
INDEX idx_locations_recorded_at (recorded_at),
INDEX idx_locations_user_recorded (user_id, recorded_at),
INDEX idx_locations_created_at (created_at)
);
location_history table
CREATE TABLE location_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
location_id UUID NOT NULL,
latitude FLOAT NOT NULL,
longitude FLOAT NOT NULL,
recorded_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE,
INDEX idx_location_history_location_id (location_id),
INDEX idx_location_history_recorded_at (recorded_at)
);
Primary Key Strategy
UUID is selected as the primary key strategy for all tables due to:
- Global uniqueness across distributed systems without coordination
- No hotspots in sharding scenarios (unlike auto-increment IDs)
- Security - prevents enumeration attacks
- Scalability - supports horizontal partitioning without ID conflicts
Index Design
| Index Name | Columns | Type | Purpose |
|---|---|---|---|
| idx_users_email | Standard | Fast user lookup by email | |
| idx_locations_user_id | user_id | Standard | Efficient location queries per user |
| idx_locations_recorded_at | recorded_at | Standard | Time-range queries for location history |
| idx_locations_user_recorded | user_id, recorded_at | Composite | Fastest queries for recent locations |
| idx_locations_created_at | created_at | Standard | Audit trail and data aging |
Data Partitioning / Sharding Strategy
Shard by user_id hash:
- Hash(user_id) % 16 to distribute users across 16 shards
- Each shard maintains its own locations table with same schema
- Partitioning key:
user_idhash modulo 16
Rationale:
- Even distribution of users across shards
- Predictable sharding logic for horizontal scaling
- Minimal cross-shard queries required
Soft Delete vs. Hard Delete Decision
Soft delete is chosen because:
- Audit requirements: Need to maintain historical location data for compliance
- Data recovery: Ability to restore accidentally deleted locations
- Analytics: Historical location patterns are valuable for analysis
- Referential integrity: Location history references deleted locations
Audit Trail Design
All data modifications capture:
created_at/updated_attimestampsuser_idfor ownership tracking (when available)- Operation type (INSERT/UPDATE/DELETE) - stored in audit log table or via event sourcing
Data Archival Strategy and Retention Policy
Archival policy:
- Locations older than 30 days are moved to cold storage (S3/BigQuery)
- Historical records retained for 1 year for compliance
- Active user locations kept in primary database
Retention rules:
- 30 days: Hot storage (fast query access)
- 1 year: Warm storage (queryable but slower)
- 5+ years: Cold storage (archive only)
-- Example archival trigger logic (simplified)
CREATE PROCEDURE archive_old_locations()
BEGIN
INSERT INTO location_archive SELECT * FROM locations
WHERE recorded_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
DELETE FROM locations
WHERE recorded_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
END
4. Class Design & Service Layer
classDiagram
class LocationService {
+trackLocation(userId, location)
+getLocationHistory(userId, startTime, endTime)
+getLiveLocation(userId)
+updateUserPreferences(userId, preferences)
}
class LocationRepository {
+saveLocation(location)
+findLocationsByUserAndTimeRange(userId, startTime, endTime)
+findLatestLocationByUser(userId)
+updateUserPreferences(userId, preferences)
}
class Location {
-userId: String
-latitude: double
-longitude: double
-timestamp: Instant
-accuracy: double
-speed: double
-bearing: double
+getDistanceTo(otherLocation)
}
class UserPreferences {
-userId: String
-trackingEnabled: boolean
-reportingInterval: int
-maxHistoryDays: int
-geofenceAlerts: boolean
}
class LocationEvent {
-userId: String
-location: Location
-timestamp: Instant
-eventType: LocationEventType
}
class LocationProcessor {
+processLocationEvent(locationEvent)
+calculateDistance(location1, location2)
}
class GeofenceService {
+checkGeofenceAlerts(location, userPreferences)
+createGeofence(userId, geofence)
+deleteGeofence(geofenceId)
}
class Geofence {
-id: String
-userId: String
-centerLatitude: double
-centerLongitude: double
-radiusMeters: double
-name: String
-alertType: GeofenceAlertType
}
class EventPublisher {
+publishLocationEvent(locationEvent)
+publishGeofenceAlert(geofenceAlert)
}
class LocationEventPublisher {
-eventPublisher: EventPublisher
-locationTopic: String
}
class LocationProcessorImpl {
-locationRepository: LocationRepository
-eventPublisher: EventPublisher
-geofenceService: GeofenceService
-locationValidator: LocationValidator
}
class LocationValidator {
+validateLocation(location)
+validateUserPreferences(preferences)
}
LocationService --> LocationRepository
LocationService --> LocationProcessorImpl
LocationProcessorImpl --> LocationRepository
LocationProcessorImpl --> EventPublisher
LocationProcessorImpl --> GeofenceService
LocationProcessorImpl --> LocationValidator
LocationEventPublisher --> EventPublisher
GeofenceService --> LocationRepository
class LocationEventType {
TRACKED
UPDATED
GEOFENCE_ENTER
GEOFENCE_EXIT
}
class GeofenceAlertType {
ENTER
EXIT
STAY
}
stateDiagram-v2
[*] --> Idle
Idle --> Tracking : startTracking()
Tracking --> Idle : stopTracking()
Tracking --> Alerting : geofenceAlert()
Alerting --> Tracking : continueTracking()
Alerting --> Idle : stopTracking()
Service Class Structure and Responsibilities
The system consists of the following key services:
- LocationService - Main entry point for location tracking operations
- LocationProcessorImpl - Core business logic processor handling location validation, geofence detection, and event publishing
- GeofenceService - Geofence management and alerting logic
- EventPublisher - Asynchronous event publishing to message broker
Interface and Abstract Class Design
public interface LocationRepository {
void saveLocation(Location location);
List<Location> findLocationsByUserAndTimeRange(String userId, Instant startTime, Instant endTime);
Location findLatestLocationByUser(String userId);
void updateUserPreferences(String userId, UserPreferences preferences);
}
public interface EventPublisher {
void publishLocationEvent(LocationEvent event);
void publishGeofenceAlert(GeofenceAlert alert);
}
public interface LocationValidator {
boolean validateLocation(Location location);
boolean validateUserPreferences(UserPreferences preferences);
}
Business Logic Encapsulation Patterns
Strategy Pattern Implementation for Location Processing
public interface LocationProcessingStrategy {
void process(LocationEvent event);
}
public class DefaultLocationProcessingStrategy implements LocationProcessingStrategy {
private final LocationRepository locationRepository;
private final EventPublisher eventPublisher;
private final GeofenceService geofenceService;
@Override
public void process(LocationEvent event) {
Location location = event.getLocation();
// Validate location data
if (!locationValidator.validateLocation(location)) {
throw new InvalidLocationException("Invalid location data received");
}
// Save the location
locationRepository.saveLocation(location);
// Check for geofence alerts
List<GeofenceAlert> alerts = geofenceService.checkGeofenceAlerts(
location,
getUserPreferences(event.getUserId())
);
// Publish alerts
for (GeofenceAlert alert : alerts) {
eventPublisher.publishGeofenceAlert(alert);
}
// Publish location event for tracking
eventPublisher.publishLocationEvent(event);
}
}
Critical-Path Method Implementation
@Transactional
public void trackLocation(String userId, Location location) {
// Validate input parameters
if (userId == null || userId.isEmpty()) {
throw new IllegalArgumentException("User ID cannot be null or empty");
}
if (location == null) {
throw new IllegalArgumentException("Location cannot be null");
}
// Validate location data
if (!locationValidator.validateLocation(location)) {
throw new InvalidLocationException("Invalid location data received");
}
// Acquire optimistic lock for user preferences to prevent concurrent updates
UserPreferences userPreferences = null;
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
userPreferences = getUserPreferencesWithLock(userId);
break;
} catch (OptimisticLockException e) {
retryCount++;
if (retryCount >= maxRetries) {
throw new ConcurrentUpdateException("Failed to acquire lock for user preferences");
}
// Brief backoff before retry
try {
Thread.sleep(50);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during lock retry", ie);
}
}
}
// Check if tracking is enabled
if (!userPreferences.isTrackingEnabled()) {
throw new TrackingDisabledException("Location tracking is disabled for user: " + userId);
}
// Store location in database
try {
locationRepository.saveLocation(location);
} catch (DataAccessException e) {
throw new LocationStorageException("Failed to store location data", e);
}
// Check for geofence alerts
List<GeofenceAlert> geofenceAlerts = geofenceService.checkGeofenceAlerts(
location,
userPreferences
);
// Publish geofence alerts if any
for (GeofenceAlert alert : geofenceAlerts) {
try {
eventPublisher.publishGeofenceAlert(alert);
} catch (Exception e) {
// Log but don't fail the main operation - alert publishing is async
logger.warn("Failed to publish geofence alert for user {}: {}", userId, e.getMessage());
}
}
// Publish location tracking event
LocationEvent locationEvent = new LocationEvent(
userId,
location,
LocationEventType.TRACKED,
Instant.now()
);
try {
eventPublisher.publishLocationEvent(locationEvent);
} catch (Exception e) {
// Log but don't fail the main operation - event publishing is async
logger.warn("Failed to publish location tracking event for user {}: {}", userId, e.getMessage());
}
// Update user preferences if needed (e.g., last tracked timestamp)
updateLastTrackedTime(userId, location.getTimestamp());
}
Transaction Boundary Design
Transaction boundaries are defined as:
- Single Location Tracking Operation - Complete atomic operation including validation, storage, and alert generation
- User Preferences Update - Separate transaction for user preference changes to avoid blocking location tracking
Service Interface Contracts
public interface LocationService {
/**
* Track a new location for a user
* @param userId The identifier of the user
* @param location The location data to track
* @throws InvalidLocationException if the location data is invalid
* @throws TrackingDisabledException if tracking is disabled for user
* @throws ConcurrentUpdateException if concurrent updates conflict
*/
void trackLocation(String userId, Location location);
/**
* Retrieve location history for a user within a time range
* @param userId The identifier of the user
* @param startTime Start of time range (inclusive)
* @param endTime End of time range (inclusive)
* @return List of location records ordered by timestamp
*/
List<Location> getLocationHistory(String userId, Instant startTime, Instant endTime);
/**
* Get the latest location for a user
* @param userId The identifier of the user
* @return Latest location record or null if none exists
*/
Location getLiveLocation(String userId);
/**
* Update user preferences for location tracking
* @param userId The identifier of the user
* @param preferences Updated preferences
*/
void updateUserPreferences(String userId, UserPreferences preferences);
}
Saga / Orchestration Pattern
For distributed transactions involving external systems (e.g., geofence alerting services), we use a saga pattern:
- Saga Coordinator - Manages the state of distributed operations
- Compensating Actions - Rollback mechanisms for failed steps
- Event Sourcing - Store saga state in database to enable recovery
Domain Object / Value Object Design
public class Location {
private final String userId;
private final double latitude;
private final double longitude;
private final Instant timestamp;
private final Double accuracy;
private final Double speed;
private final Double bearing;
// Constructor, getters, equals, hashCode
}
public class UserPreferences {
private final String userId;
private final boolean trackingEnabled;
private final int reportingInterval; // seconds
private final int maxHistoryDays;
private final boolean geofenceAlerts;
// Constructor, getters, equals, hashCode
}
public class Geofence {
private final String id;
private final String userId;
private final double centerLatitude;
private final double centerLongitude;
private final double radiusMeters;
private final String name;
private final GeofenceAlertType alertType;
// Constructor, getters, equals, hashCode
}
5. Concurrency & Thread Safety
Concurrency Model
The location tracking system will use a thread-safe, asynchronous processing model with:
- Java Virtual Threads (JDK 21+) for handling concurrent location updates from mobile clients
- Reactive programming model using Project Reactor for non-blocking I/O operations
- Single-threaded event loop per location tracking session to maintain consistency
- Thread pools for database operations and async event publishing
Race Condition Prevention Strategy
Race conditions will be prevented through:
- Immutable location objects - all location data is immutable once created
- Atomic operations for timestamp-based ordering and conflict resolution
- Optimistic locking using version numbers in the database for concurrent updates
- Read-then-update patterns with proper synchronization to prevent lost updates
Optimistic vs. Pessimistic Locking Selection
Optimistic locking is selected for the following reasons:
- High write concurrency expected from mobile devices (thousands of concurrent updates)
- Low probability of actual conflicts in location data (timestamp-based ordering is sufficient)
- Better performance under high throughput scenarios
- Reduced lock contention and improved system responsiveness
Deadlock Prevention Strategy
Deadlock prevention mechanisms:
- Fixed lock ordering - all database operations follow consistent ordering (by entity ID, then timestamp)
- Timeout-based lock acquisition - 500ms timeout for database locks
- Hierarchical resource management - location entity locks acquired before session locks
- Transaction isolation level set to READ_COMMITTED to prevent phantom reads
Idempotency Implementation for Concurrent Duplicate Requests
Idempotency will be handled through:
- Client-generated unique request IDs (UUIDs) sent with each location update
- Database-level deduplication using unique constraints on (client_id, request_id)
- In-memory cache of recent request IDs with TTL of 5 minutes for fast rejection
- Idempotency key in HTTP headers (
X-Request-ID) with validation in middleware
// Idempotency check logic example
public class LocationUpdateIdempotencyService {
private final Map<String, Long> recentRequests = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public LocationUpdateIdempotencyService() {
// Cleanup expired entries every minute
scheduler.scheduleAtFixedRate(
() -> recentRequests.entrySet().removeIf(entry ->
System.currentTimeMillis() - entry.getValue() > 300000),
60, 60, TimeUnit.SECONDS);
}
public boolean isIdempotent(String clientId, String requestId) {
String key = clientId + ":" + requestId;
Long timestamp = recentRequests.putIfAbsent(key, System.currentTimeMillis());
return timestamp != null;
}
}
Thread Safety Patterns in Core Components
Immutable Location Data Objects:
public final class Location {
private final String id;
private final double latitude;
private final double longitude;
private final long timestamp;
private final String deviceId;
private final double accuracy;
public Location(String id, double latitude, double longitude, long timestamp,
String deviceId, double accuracy) {
this.id = Objects.requireNonNull(id);
this.latitude = latitude;
this.longitude = longitude;
this.timestamp = timestamp;
this.deviceId = Objects.requireNonNull(deviceId);
this.accuracy = accuracy;
}
// All getters, no setters - immutable object
}
Thread-safe Repository Pattern:
@Repository
public class LocationRepository {
private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper;
public LocationRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
this.jdbcTemplate = jdbcTemplate;
this.objectMapper = objectMapper;
}
@Transactional
public void saveLocation(Location location) {
// Using optimistic locking with version field
String sql = "INSERT INTO locations (id, latitude, longitude, timestamp, device_id, accuracy, version) VALUES (?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sql,
location.getId(),
location.getLatitude(),
location.getLongitude(),
location.getTimestamp(),
location.getDeviceId(),
location.getAccuracy(),
1); // Initial version
}
@Transactional
public boolean updateLocationWithVersion(String id, long currentVersion, Location updatedLocation) {
String sql = "UPDATE locations SET latitude=?, longitude=?, timestamp=?, accuracy=?, version=? WHERE id=? AND version=?";
int rowsAffected = jdbcTemplate.update(sql,
updatedLocation.getLatitude(),
updatedLocation.getLongitude(),
updatedLocation.getTimestamp(),
updatedLocation.getAccuracy(),
currentVersion + 1,
id,
currentVersion);
return rowsAffected > 0;
}
}
Thread-safe Session Management:
@Component
public class LocationSessionManager {
private final Map<String, LocationSession> activeSessions = new ConcurrentHashMap<>();
public void registerSession(String sessionId, String deviceId) {
activeSessions.put(sessionId, new LocationSession(deviceId));
}
public boolean isSessionActive(String sessionId) {
return activeSessions.containsKey(sessionId);
}
public void unregisterSession(String sessionId) {
activeSessions.remove(sessionId);
}
}
Concurrency Control in High-Volume Scenarios
For high-volume location tracking:
- Batch processing of location updates to reduce database load
- Async processing with bounded thread pools (max 100 threads)
- Rate limiting per device to prevent abuse
- Partitioned database writes by device ID for better concurrency
Performance Considerations
- Lock-free data structures where possible (ConcurrentHashMap, AtomicLong)
- Batch operations for multiple location updates from same device
- Read-only operations using database snapshots to avoid blocking reads
- Connection pooling with proper timeout configuration (max 50 connections, 30s timeout)
6. Sequence Diagrams for Key Flows
sequenceDiagram
participant U as User/Client
participant A as API Gateway
participant S as Location Service
participant DB as Database
participant C as Cache
participant M as Message Broker
U->>A: POST /locations
A->>S: Forward request
S->>S: Validate request
S->>C: Check if location exists in cache
C-->>S: Cache miss
S->>DB: Insert new location
DB-->>S: Location saved
S->>C: Cache location
C-->>S: Cached successfully
S->>M: Publish LOCATION_UPDATED event
M-->>S: Event queued
S-->>A: 201 Created
A-->>U: Location created successfully
sequenceDiagram
participant U as User/Client
participant A as API Gateway
participant S as Location Service
participant DB as Database
participant C as Cache
U->>A: GET /locations/{id}
A->>S: Forward request
S->>C: Check if location exists in cache
C-->>S: Cache hit - return location
S-->>A: 200 OK
A-->>U: Location data returned
Note over S,C: Cache miss scenario
C->>S: Cache miss
S->>DB: Fetch location from DB
DB-->>S: Location data returned
S->>C: Cache location for future requests
C-->>S: Cached successfully
S-->>A: 200 OK
A-->>U: Location data returned
sequenceDiagram
participant U as User/Client
participant A as API Gateway
participant S as Location Service
participant DB as Database
participant M as Message Broker
U->>A: POST /locations/track
A->>S: Forward request
S->>S: Validate tracking request
S->>DB: Insert location update
DB-->>S: Location updated
S->>M: Publish LOCATION_UPDATED event
M-->>S: Event queued
S->>S: Update cache with latest location
S-->>A: 200 OK
A-->>U: Tracking updated successfully
Note over S,M: Async event processing
M->>M: Process LOCATION_UPDATED event
M->>S: Trigger location analytics update
S-->>M: Analytics updated
7. Caching Design
Cache Strategy Selection
For location tracking, we'll use cache-aside pattern with LRU eviction policy. This approach provides the best balance between performance and consistency for read-heavy workloads with frequent location updates.
Cache Key Design
Cache keys will follow a structured namespace pattern:
location:{userId}:{timestamp}:{accuracy}
location:recent:{userId}:{maxAge}
location:history:{userId}:{date}
TTL Strategy
| Data Type | TTL | Reason |
|---|---|---|
| Recent locations | 5 minutes | High frequency updates, recent data is most valuable |
| Historical locations | 30 days | Compliance and analytics requirements |
| User profile cache | 1 hour | Stable user data, infrequent changes |
| Device metadata | 24 hours | Device info changes rarely |
Cache Eviction Policy
LRU (Least Recently Used) with probabilistic early expiry to prevent cache stampede:
- Primary eviction: LRU
- Secondary: 10% probabilistic early expiry to prevent thundering herd on cache misses
Cache Stampede Prevention
- Probabilistic Early Expiry: 10% of cache entries expire 2 minutes early
- Distributed Locking: For hot keys, use Redis distributed locks during cache rebuilds
- Cache Warm-up: Pre-warm popular location caches during low-traffic periods
Distributed Cache Architecture
Using Redis Cluster with:
- 3 master nodes for data distribution
- 3 replica nodes per shard for high availability
- Redis Sentinel for automatic failover
- Memory policy:
allkeys-lruwith 2GB per node limit
Cache Invalidation Strategy
Event-driven invalidation using message broker:
- When location is updated → invalidate
location:{userId}:{timestamp} - When user profile changes → invalidate
location:recent:{userId}:* - When device is unregistered → invalidate all
location:{deviceId}:*
Cache Read Path
public Location getLocationWithCaching(String userId, long timestamp) {
String cacheKey = "location:" + userId + ":" + timestamp;
// Try cache first
Location cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// Cache miss - acquire distributed lock
String lockKey = "lock:" + cacheKey;
if (redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", Duration.ofSeconds(10))) {
try {
// Double-check cache after lock acquisition
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// Fetch from database
Location location = locationRepository.findByUserIdAndTimestamp(userId, timestamp);
// Cache for 5 minutes
redisTemplate.opsForValue().set(cacheKey, location, Duration.ofMinutes(5));
return location;
} finally {
// Release lock
redisTemplate.delete(lockKey);
}
} else {
// Wait briefly and retry
Thread.sleep(50);
return getLocationWithCaching(userId, timestamp);
}
}
Cache Write Path
public void updateLocationWithCaching(String userId, Location location) {
String cacheKey = "location:" + userId + ":" + location.getTimestamp();
// Update database
locationRepository.save(location);
// Invalidate cache entries
String recentKeyPattern = "location:recent:" + userId + ":*";
Set<String> keysToInvalidate = redisTemplate.keys(recentKeyPattern);
if (!keysToInvalidate.isEmpty()) {
redisTemplate.delete(keysToInvalidate);
}
// Set new cache entry
redisTemplate.opsForValue().set(cacheKey, location, Duration.ofMinutes(5));
}
8. Repository / Data Access Layer
Repository Pattern Implementation
The repository pattern is implemented to abstract data access logic and provide a clean separation between domain logic and persistence concerns. Each entity will have its own repository interface and implementation.
public interface LocationRepository {
void save(Location location);
Optional<Location> findById(UUID id);
List<Location> findByUserId(UUID userId);
List<Location> findByUserIdAndTimeRange(UUID userId, Instant startTime, Instant endTime);
void deleteById(UUID id);
void deleteByUserId(UUID userId);
}
public class LocationRepositoryImpl implements LocationRepository {
private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper;
public LocationRepositoryImpl(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
this.jdbcTemplate = jdbcTemplate;
this.objectMapper = objectMapper;
}
@Override
public void save(Location location) {
String sql = """
INSERT INTO locations (id, user_id, latitude, longitude, altitude, accuracy, timestamp, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""";
jdbcTemplate.update(sql,
location.getId(),
location.getUserId(),
location.getLatitude(),
location.getLongitude(),
location.getAltitude(),
location.getAccuracy(),
location.getTimestamp(),
objectMapper.writeValueAsString(location.getMetadata())
);
}
@Override
public Optional<Location> findById(UUID id) {
String sql = "SELECT * FROM locations WHERE id = ?";
try {
return Optional.of(jdbcTemplate.queryForObject(sql, new LocationRowMapper(), id));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public List<Location> findByUserId(UUID userId) {
String sql = "SELECT * FROM locations WHERE user_id = ? ORDER BY timestamp ASC";
return jdbcTemplate.query(sql, new LocationRowMapper(), userId);
}
@Override
public List<Location> findByUserIdAndTimeRange(UUID userId, Instant startTime, Instant endTime) {
String sql = """
SELECT * FROM locations
WHERE user_id = ? AND timestamp BETWEEN ? AND ?
ORDER BY timestamp ASC
""";
return jdbcTemplate.query(sql, new LocationRowMapper(), userId, startTime, endTime);
}
@Override
public void deleteById(UUID id) {
String sql = "DELETE FROM locations WHERE id = ?";
jdbcTemplate.update(sql, id);
}
@Override
public void deleteByUserId(UUID userId) {
String sql = "DELETE FROM locations WHERE user_id = ?";
jdbcTemplate.update(sql, userId);
}
}
ORM vs. Raw Query Decisions
For location tracking, raw SQL queries are preferred over ORM due to:
- Performance: Raw queries avoid unnecessary object mapping overhead
- Complex aggregations: Location data often requires time-series analytics that are better expressed with SQL
- Index optimization: Direct control over query execution plans
Read vs. Write Separation (CQRS Pattern)
For high-scale location tracking, a CQRS approach is implemented:
public interface LocationReadRepository {
List<Location> findRecentLocations(UUID userId, int limit);
List<Location> findLocationHistory(UUID userId, Instant start, Instant end);
LocationSummary getLatestLocationSummary(UUID userId);
}
public interface LocationWriteRepository {
void saveBatch(List<Location> locations);
void deleteOldLocations(Instant cutoffTime);
}
Connection Pool Configuration
Connection pool settings optimized for high-throughput location writes:
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
leak-detection-threshold: 60000
N+1 Query Prevention Strategy
Location data is fetched with pre-loaded metadata and aggregated statistics:
public class LocationWithStats {
private Location location;
private LocationStatistics stats;
// getters and setters
}
public class LocationQueryService {
public List<LocationWithStats> fetchLocationsWithStats(UUID userId, int limit) {
String sql = """
SELECT l.*,
COUNT(*) OVER() as total_count,
AVG(l.latitude) as avg_latitude,
AVG(l.longitude) as avg_longitude
FROM locations l
WHERE l.user_id = ?
ORDER BY l.timestamp DESC
LIMIT ?
""";
return jdbcTemplate.query(sql, new LocationWithStatsRowMapper(), userId, limit);
}
}
Batch Operation Design
Batch operations for high-volume location updates:
public class LocationBatchService {
public void saveLocationsInBatch(List<Location> locations) {
String sql = """
INSERT INTO locations (id, user_id, latitude, longitude, altitude, accuracy, timestamp, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""";
jdbcTemplate.batchUpdate(sql, locations, locations.size(), (ps, location) -> {
ps.setObject(1, location.getId());
ps.setObject(2, location.getUserId());
ps.setDouble(3, location.getLatitude());
ps.setDouble(4, location.getLongitude());
ps.setDouble(5, location.getAltitude());
ps.setDouble(6, location.getAccuracy());
ps.setTimestamp(7, Timestamp.from(location.getTimestamp()));
ps.setString(8, objectMapper.writeValueAsString(location.getMetadata()));
});
}
}
Data Access Layer Interfaces
public interface LocationService {
void trackLocation(UUID userId, LocationUpdate update);
List<Location> getRecentLocations(UUID userId, int limit);
List<Location> getLocationHistory(UUID userId, Instant start, Instant end);
void deleteUserLocations(UUID userId);
}
Transaction Management
@Service
@Transactional
public class LocationTrackingService implements LocationService {
private final LocationRepository locationRepository;
private final LocationEventPublisher eventPublisher;
public void trackLocation(UUID userId, LocationUpdate update) {
Location location = Location.builder()
.id(UUID.randomUUID())
.userId(userId)
.latitude(update.getLatitude())
.longitude(update.getLongitude())
.altitude(update.getAltitude())
.accuracy(update.getAccuracy())
.timestamp(Instant.now())
.metadata(update.getMetadata())
.build();
locationRepository.save(location);
// Publish event for async processing
eventPublisher.publishLocationTracked(location);
}
}
Query Optimization
Index usage for optimal query performance:
-- Primary index for location lookups by user and time
CREATE INDEX idx_locations_user_time ON locations(user_id, timestamp);
-- Composite index for location history queries
CREATE INDEX idx_locations_user_time_alt ON locations(user_id, timestamp, altitude);
-- Partial index for recent location queries (last 30 days)
CREATE INDEX idx_locations_recent ON locations(user_id, timestamp)
WHERE timestamp > NOW() - INTERVAL '30 days';
Data Access Layer Testing
@ExtendWith(MockitoExtension.class)
class LocationRepositoryTest {
@Mock
private JdbcTemplate jdbcTemplate;
@InjectMocks
private LocationRepositoryImpl repository;
@Test
void shouldSaveLocation() {
Location location = createSampleLocation();
repository.save(location);
verify(jdbcTemplate).update(anyString(),
eq(location.getId()),
eq(location.getUserId()),
eq(location.getLatitude()),
eq(location.getLongitude()),
eq(location.getAltitude()),
eq(location.getAccuracy()),
eq(location.getTimestamp()),
anyString()
);
}
}
9. Asynchronous Processing & Messaging
Event-driven boundaries in this feature
The location tracking system will use a hybrid synchronous/asynchronous approach. Real-time location updates from mobile devices will be processed synchronously for immediate validation and storage, but historical location data and analytics will be handled asynchronously to avoid blocking user operations.
Key async triggers:
- Location update events: When a device sends location data, it's processed asynchronously for analytics and long-term storage
- Geofence crossing events: When a user crosses a defined boundary, notifications and business logic are triggered asynchronously
- Batch location aggregation: Periodic batch processing of location data for analytics and reporting
Message schema design with sample payload
{
"version": "1.0",
"event_type": "location.update",
"timestamp": "2023-11-15T14:30:00Z",
"user_id": "user-12345",
"location": {
"latitude": 40.7128,
"longitude": -74.0060,
"accuracy": 5.2,
"altitude": 10.0,
"timestamp": "2023-11-15T14:30:00Z"
},
"device_info": {
"device_id": "device-abc123",
"os_version": "iOS 17.0",
"app_version": "2.1.0"
}
}
Dead letter queue strategy and poison message handling
Poison messages will be handled through a dead-letter queue (DLQ) with a maximum retry limit of 3 attempts. Messages that fail after 3 retries will be moved to a DLQ topic with a retention period of 7 days. A separate monitoring system will alert on DLQ message accumulation and trigger manual investigation.
At-least-once vs. exactly-once delivery
At-least-once delivery is selected for location tracking events because:
- Location updates are idempotent (redundant updates don't change system state)
- The primary concern is ensuring all location data is processed for analytics and geofence detection
- The cost of duplicate processing is minimal compared to missing data
Retry policy for consumers
Retry policy configuration:
- Maximum attempts: 3
- Backoff strategy: Exponential with jitter (1s, 2s, 4s)
- Retryable errors: Network timeouts, database connection failures
- Non-retryable errors: Invalid location data format, unauthorized user
public class RetryPolicy {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
public <T> T executeWithRetry(Supplier<T> operation) {
int attempts = 0;
while (true) {
try {
return operation.get();
} catch (Exception e) {
attempts++;
if (attempts >= MAX_RETRIES || !isRetryable(e)) {
throw new RetryExhaustedException("Operation failed after " + attempts + " attempts", e);
}
long delay = (long) Math.pow(2, attempts) * BASE_DELAY_MS;
delay += ThreadLocalRandom.current().nextLong(0, 500); // Add jitter
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RetryExhaustedException("Retry interrupted", ie);
}
}
}
}
private boolean isRetryable(Exception e) {
return e instanceof NetworkException ||
e instanceof DatabaseException ||
e instanceof TimeoutException;
}
}
Outbox pattern for reliable event publishing
The outbox pattern is implemented to ensure eventual consistency between location data persistence and event publishing:
public class LocationEventPublisher {
private final EventRepository eventRepository;
private final KafkaTemplate<String, LocationUpdateEvent> kafkaTemplate;
public void publishLocationUpdate(LocationUpdate location) {
// 1. Store location to database
Location storedLocation = locationRepository.save(location);
// 2. Create event in outbox table
LocationUpdateEvent event = new LocationUpdateEvent(
storedLocation.getId(),
storedLocation.getUserId(),
storedLocation.getCoordinates(),
storedLocation.getTimestamp()
);
eventRepository.save(new OutboxEvent(
UUID.randomUUID(),
"location.update",
event,
EventStatus.PENDING
));
// 3. Process outbox asynchronously (in separate thread or worker)
eventProcessor.processPendingEvents();
}
}
Sequence diagram for async flows
sequenceDiagram
participant MobileApp
participant APIGateway
participant LocationService
participant Database
participant EventPublisher
participant Kafka
participant AnalyticsService
participant GeofenceService
MobileApp->>APIGateway: POST /locations
APIGateway->>LocationService: Process location update
LocationService->>Database: Save location data
LocationService->>EventPublisher: Publish to outbox
EventPublisher->>Kafka: Send location.update event
Kafka->>AnalyticsService: Consume for analytics
Kafka->>GeofenceService: Consume for geofence detection
10. Error Handling Design
Error Classification Hierarchy
The system follows a three-level error classification hierarchy:
- Domain Errors - Business logic violations (e.g., invalid location data, exceeded rate limits)
- Infrastructure Errors - System-level issues (e.g., database connection failures, cache unavailability)
- Validation Errors - Input format issues (e.g., malformed coordinates, missing required fields)
public class ErrorType {
public enum Category {
DOMAIN,
INFRASTRUCTURE,
VALIDATION
}
public enum Code {
INVALID_LOCATION_DATA(1001, Category.VALIDATION),
LOCATION_SERVICE_UNAVAILABLE(2001, Category.INFRASTRUCTURE),
RATE_LIMIT_EXCEEDED(3001, Category.DOMAIN);
private final int code;
private final Category category;
Code(int code, Category category) {
this.code = code;
this.category = category;
}
public int getCode() { return code; }
public Category getCategory() { return category; }
}
}
Error Propagation Strategy
Errors are propagated up the call stack with appropriate wrapping and logging. Service layer catches infrastructure errors and rethrows them as domain errors when appropriate.
Retry Strategy
For transient infrastructure errors, exponential backoff with jitter is applied:
public class RetryStrategy {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
private static final double JITTER_RANGE = 0.2;
public static boolean shouldRetry(Exception ex) {
return ex instanceof InfrastructureException ||
ex.getCause() instanceof ConnectionException;
}
public static long calculateDelay(int attempt) {
long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempt));
double jitter = (Math.random() * 2 - 1) * JITTER_RANGE;
return delay + (long) (delay * jitter);
}
}
Circuit Breaker Design
For external dependencies like geocoding services, a circuit breaker pattern is implemented:
public class CircuitBreaker {
private final int failureThreshold = 5;
private final long timeout = 30000; // 30 seconds
private final int resetTimeout = 60000; // 1 minute
private volatile CircuitState state = CircuitState.CLOSED;
private int failureCount = 0;
private long lastFailureTime = 0;
public enum CircuitState {
CLOSED, OPEN, HALF_OPEN
}
public boolean canExecute() {
if (state == CircuitState.CLOSED) return true;
if (state == CircuitState.HALF_OPEN && failureCount == 0) return true;
if (state == CircuitState.OPEN &&
System.currentTimeMillis() - lastFailureTime > resetTimeout) {
state = CircuitState.HALF_OPEN;
return true;
}
return false;
}
public void recordSuccess() {
if (state == CircuitState.HALF_OPEN) {
state = CircuitState.CLOSED;
}
}
public void recordFailure() {
if (state == CircuitState.CLOSED) {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (failureCount >= failureThreshold) {
state = CircuitState.OPEN;
}
} else if (state == CircuitState.HALF_OPEN) {
state = CircuitState.OPEN;
}
}
}
Fallback Behavior Design
For location tracking operations, fallbacks are implemented for critical dependencies:
public class LocationService {
private final GeocodingService geocodingService;
private final CircuitBreaker circuitBreaker = new CircuitBreaker();
public LocationData fallbackLocationLookup(String coordinates) {
// Return default location or cached data
return new LocationData(0.0, 0.0, "DEFAULT_LOCATION");
}
public LocationData getLocationWithFallback(String coordinates) {
try {
if (circuitBreaker.canExecute()) {
LocationData result = geocodingService.lookup(coordinates);
circuitBreaker.recordSuccess();
return result;
} else {
return fallbackLocationLookup(coordinates);
}
} catch (Exception ex) {
circuitBreaker.recordFailure();
return fallbackLocationLookup(coordinates);
}
}
}
Error Logging Format
Structured logging includes trace context and error metadata:
{
"timestamp": "2023-10-15T14:30:45.123Z",
"trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"span_id": "f0e9d8c7-b6a5-4321-fedc-ba9876543210",
"user_id": "user-12345",
"error_code": "INVALID_LOCATION_DATA",
"error_message": "Coordinates must be between -180 and 180 degrees",
"error_type": "VALIDATION",
"component": "LocationService",
"request_context": {
"coordinates": "invalid-coords",
"timestamp": "2023-10-15T14:30:45.123Z"
}
}
Alerting Thresholds
Critical errors trigger alerts at warn/critical levels:
| Error Code | Severity | Alert Threshold | Description |
|---|---|---|---|
| INVALID_LOCATION_DATA | WARN | 100 per hour | Validation errors |
| LOCATION_SERVICE_UNAVAILABLE | CRITICAL | 5 per minute | Service outages |
| RATE_LIMIT_EXCEEDED | INFO | 1000 per hour | User throttling |
Error Budget Considerations
Infrastructure errors that exceed 5% of total requests trigger alerting. The system maintains a 99.9% error budget for critical operations, with alerts triggered at 0.1% threshold for proactive monitoring.
Error Recovery Patterns
For transient errors, the system implements:
- Retry with exponential backoff for database connection issues
- Fallback to cached data for geocoding service failures
- Graceful degradation of location accuracy for network timeouts
11. Security Design
Input Validation and Sanitization Strategy
All inputs are validated using a combination of:
- Regex patterns for format validation (e.g., email, phone number)
- Length constraints to prevent buffer overflow attacks
- Type checking to ensure correct data types
- Custom validators for business logic constraints (e.g., valid location coordinates)
public class LocationValidator {
private static final Pattern COORDINATE_PATTERN =
Pattern.compile("^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$");
public static boolean isValidCoordinate(String coordinate) {
return coordinate != null && COORDINATE_PATTERN.matcher(coordinate).matches();
}
}
SQL Injection Prevention
All database queries use parameterized prepared statements exclusively. No dynamic query construction or string concatenation is permitted.
// Secure approach - parameterized query
String sql = "SELECT * FROM location_events WHERE user_id = ? AND timestamp BETWEEN ? AND ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, userId);
stmt.setTimestamp(2, startTime);
stmt.setTimestamp(3, endTime);
Authentication Middleware Design
Authentication is handled via JWT tokens with the following flow:
- User authenticates via OAuth2 or username/password
- System generates JWT with user ID, role, and expiration
- Middleware validates token on every request
- Token refresh mechanism is implemented via refresh tokens
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtService.validateToken(token)) {
String userId = jwtService.extractUserId(token);
// Set security context
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList())
);
}
filterChain.doFilter(request, response);
}
}
Authorization Enforcement
Role-based access control (RBAC) is implemented:
- Users can only access their own location data
- Admins have full read/write access to all location events
- Service accounts can only access data within their scope
@PreAuthorize("hasRole('ADMIN') or @locationAuthorizationService.canAccessLocation(#userId)")
public List<LocationEvent> getUserLocationEvents(String userId) {
return locationRepository.findByUserId(userId);
}
Sensitive Data Encryption at Rest and in Transit
- At rest: AES-256 encryption for sensitive location data fields (latitude, longitude)
- In transit: TLS 1.3+ for all API communications
- Key management: AWS KMS integration for key rotation and management
@Service
public class DataEncryptionService {
private final AesGcmEncryptor encryptor = new AesGcmEncryptor();
public String encryptLocationData(String data) {
return encryptor.encrypt(data, encryptionKey);
}
public String decryptLocationData(String encryptedData) {
return encryptor.decrypt(encryptedData, encryptionKey);
}
}
PII Handling and Masking Strategy
Location coordinates are masked for non-privileged users:
- Full precision only available to admins or data owners
- For general users, coordinates are rounded to 4 decimal places (within ~10m accuracy)
- All PII is logged with proper masking in audit trails
public class LocationMaskingService {
public static String maskCoordinate(String coordinate) {
if (coordinate == null) return null;
BigDecimal value = new BigDecimal(coordinate);
return value.setScale(4, RoundingMode.HALF_UP).toString();
}
}
Secrets Management
Secrets are managed using AWS Secrets Manager:
- API keys, database credentials, encryption keys stored securely
- Automatic rotation every 90 days
- Least privilege principle - each service has minimal required permissions
Audit Logging Design
All security-relevant events are logged with:
- Timestamp, user ID, action type, IP address, and success/failure status
- PII is masked in logs (coordinates truncated to 4 decimal places)
- Logs are stored for 90 days with compliance retention policy
@EventListener
public void handleSecurityEvent(SecurityEvent event) {
auditLogger.info("Security Event: {}, User: {}, IP: {}, Success: {}",
event.getAction(), event.getUserId(),
event.getIpAddress(), event.isSuccess());
}
12. Testing Strategy
Unit Testing Scope and Coverage Targets
- Target coverage: 85% line coverage, 90% branch coverage for core business logic
- Scope: All service layer classes, repository methods, domain objects, and utility functions
- Test focus areas:
- Location tracking business logic (distance calculation, geofence validation)
- Data model validation and transformation
- Error handling paths in service methods
- Concurrency control logic (optimistic locking, race condition prevention)
- Event publishing and async processing flows
Integration Testing Strategy
- Database integration: Use Testcontainers with PostgreSQL 13+ for real database testing
- External service mocks: WireMock for third-party geocoding APIs, Redis mock for caching layer
- Cross-service dependencies: Embedded Kafka or in-memory message broker for async flows
- Test data setup: Factory pattern with realistic location data including edge cases (null coordinates, invalid ranges)
- Test isolation: Per-test database schema or in-memory database instances to prevent test pollution
Contract Testing
- Framework: Pact for consumer-driven contract testing between location service and dependent services (e.g., analytics, notifications)
- Contract scope: All external API endpoints and event schemas
- Versioning strategy: SemVer-based contract versioning with backward compatibility checks
- Testing frequency: Run contract tests in CI pipeline on every push to main branch
Performance Testing Tool and Key Scenarios
- Tool: k6 for load testing with realistic concurrent user scenarios
- Key performance scenarios:
- High-volume location updates (1000 concurrent users updating every 5 seconds)
- Real-time geofence notifications (simultaneous entry/exit events for 1000 users)
- Batch location history queries (500 concurrent historical data requests)
- Metrics to measure:
- p95 response time < 200ms for location updates
- Throughput: >5000 location events/sec under load
- Memory usage < 2GB for 10k concurrent sessions
Test Data Management Strategy
- Fixtures: Pre-defined location data sets for common scenarios (urban, rural, edge cases)
- Factories: Factory classes to generate realistic user location data with randomization within constraints
- Seeding strategy: Database seeding using JSON fixtures for initial test data, with factory-generated data for dynamic test scenarios
- Data cleanup: Automatic schema cleanup between tests using transaction rollback or database reset
- Privacy compliance: All test data is synthetic and does not contain real user PII
// Example unit test for location validation logic
@Test
public void testLocationValidationWithInvalidCoordinates() {
LocationRequest request = new LocationRequest();
request.setLatitude(91.0); // Invalid latitude
request.setLongitude(181.0); // Invalid longitude
assertThrows(InvalidLocationException.class, () -> {
locationService.validateAndProcess(request);
});
}
@Test
public void testGeofenceEntryNotification() {
User user = createUserWithGeofence();
Location location = createLocationInGeofence();
// Mock event publisher to capture notification
Mockito.doNothing().when(eventPublisher).publish(Mockito.any());
locationService.processLocationUpdate(user.getId(), location);
Mockito.verify(eventPublisher, Mockito.times(1)).publish(Mockito.any());
}
13. Observability Design
Structured Logging Schema
All logs must follow a structured JSON format with consistent fields to enable efficient querying and analysis:
{
"timestamp": "2023-10-27T14:30:45.123Z",
"level": "INFO|WARN|ERROR",
"service": "location-tracking-service",
"trace_id": "12345678-1234-1234-1234-123456789012",
"span_id": "abcdef12-3456-7890-abcd-ef1234567890",
"user_id": "user_12345",
"entity_id": "location_98765",
"operation": "trackLocation",
"request_id": "req_abcde12345",
"message": "Location updated successfully",
"error_code": "LOCATION_UPDATE_FAILED",
"error_details": {
"cause": "Database connection timeout",
"stack_trace": "...",
"retry_count": 2
},
"duration_ms": 45,
"metadata": {
"latitude": 40.7128,
"longitude": -74.0060
}
}
PII masking rules:
- User ID and entity IDs are logged as-is if they're UUIDs or numeric IDs
- Full PII (names, emails) is never logged in clear text; masked or omitted entirely
- GPS coordinates are logged with precision limited to 6 decimal places (10cm accuracy)
Distributed Tracing Instrumentation
Using OpenTelemetry with custom spans for key operations:
Span Types and Attributes:
-
location.track- Attributes:
user_id,request_id,latitude,longitude,accuracy - Custom attributes:
location_update_type(REALTIME, BATCH),device_id
- Attributes:
-
database.location.save- Attributes:
db.operation,db.statement,db.connection_id - Custom attributes:
retry_count,is_retry
- Attributes:
-
cache.location.get- Attributes:
cache.hit,cache.key - Custom attributes:
cache.ttl_seconds
- Attributes:
Sample Trace Flow:
[Root Span] location.track
├── [Span] database.location.save
│ ├── [Span] database.connection.acquire
│ └── [Span] database.query.execute
└── [Span] cache.location.get
├── [Span] cache.key.generate
└── [Span] cache.lookup
Metrics Design
RED Method Metrics (Rate, Errors, Duration)
| Metric Name | Type | Labels | Description |
|---|---|---|---|
location_updates_total | Counter | status, user_id, update_type | Total location updates processed |
location_update_errors_total | Counter | error_code, user_id | Errors during location updates |
location_update_duration_seconds | Histogram | status, user_id | Duration of location update operations |
location_cache_hits_total | Counter | cache_type, user_id | Cache hits for location data |
location_cache_misses_total | Counter | cache_type, user_id | Cache misses for location data |
location_database_queries_total | Counter | operation, status | Database query counts |
location_database_query_duration_seconds | Histogram | operation, status | Database query execution times |
Additional Gauge Metrics:
location_active_users(Gauge): Current number of users with active location trackinglocation_cache_size(Gauge): Current size of location cache entries
Health Check Endpoint Design
Endpoint: /health
{
"status": "healthy|unhealthy",
"checks": {
"database": {
"status": "healthy",
"latency_ms": 12
},
"cache": {
"status": "healthy",
"latency_ms": 3
},
"message_queue": {
"status": "healthy",
"latency_ms": 5
}
},
"timestamp": "2023-10-27T14:30:45.123Z"
}
Endpoint: /ready
{
"status": "ready|not_ready",
"components": {
"database": "ready",
"cache": "ready",
"message_queue": "ready"
}
}
Endpoint: /live
{
"status": "alive",
"uptime_seconds": 3600,
"version": "1.2.3"
}
Alerting Thresholds
| Metric | Alert Level | Threshold | Frequency | Action |
|---|---|---|---|---|
location_update_errors_total | Warning | > 5 errors/minute | Every minute | Slack alert |
location_update_errors_total | Critical | > 50 errors/minute | Every minute | On-call escalation |
location_update_duration_seconds | Warning | > 500ms p95 | Every 5 minutes | Slack alert |
location_update_duration_seconds | Critical | > 2000ms p95 | Every 5 minutes | On-call escalation |
location_database_query_duration_seconds | Warning | > 1000ms p95 | Every 5 minutes | Slack alert |
location_database_query_duration_seconds | Critical | > 3000ms p95 | Every 5 minutes | On-call escalation |
location_cache_misses_total | Warning | > 1000 misses/minute | Every minute | Slack alert |
location_cache_misses_total | Critical | > 5000 misses/minute | Every minute | On-call escalation |
Alerting Rules:
- All alerts use 5-minute sliding window for stability
- Critical alerts trigger on-call notifications with escalation policies
- Warning alerts are logged to Slack channels for monitoring
- Alert thresholds are based on historical performance data and SLO targets
14. Open Questions, Risks & Trade-offs
Decisions requiring product / engineering alignment
- Data retention policy: How long should location data be retained? This directly impacts storage costs, compliance requirements, and user privacy expectations. Should we implement a tiered retention strategy (e.g., 1 week for basic tracking, 30 days for premium)?
- Real-time vs. batch processing: Should location updates be processed in real-time or batched for efficiency? Real-time processing provides better UX but increases infrastructure costs and complexity.
- Geofence configuration: How granular should geofence definitions be? Should we support custom polygon shapes or restrict to simple circles/rectangles for performance?
- Cross-platform consistency: Should the same location tracking logic apply across mobile, web, and IoT devices? Different platforms may have varying accuracy capabilities and battery constraints.
Known trade-offs and justifications
| Trade-off | Option A | Option B | Justification |
|---|---|---|---|
| Storage vs. Query Performance | Store raw GPS points with minimal processing | Pre-aggregate and store location clusters | Raw points provide maximum flexibility for analytics but increase storage costs and query complexity. Aggregation reduces storage but limits analytical capabilities. |
| Consistency vs. Availability | Strong consistency for location updates | Eventual consistency with optimistic conflict resolution | Strong consistency ensures data accuracy but may impact availability during network partitions. Eventual consistency improves availability but requires conflict resolution logic. |
| Precision vs. Battery Life | High-frequency location updates (1s) | Low-frequency updates (30s-1min) | High frequency provides better tracking accuracy but drains battery quickly. Lower frequency conserves battery but may miss important location changes. |
| Single Source vs. Multi-Source Tracking | Rely solely on GPS for location data | Combine GPS, WiFi, cellular, and sensor data | Single source is simpler to implement but less accurate in urban environments or indoors. Multi-source provides better accuracy but increases complexity and requires more sophisticated fusion algorithms. |
Risks and mitigation strategies
- High storage costs: Location data grows rapidly with high-frequency updates. Mitigation: Implement intelligent sampling and compression strategies, use data archiving for older records, and establish clear retention policies.
- Privacy compliance violations: Risk of violating privacy regulations (GDPR, CCPA) if location data is not properly handled. Mitigation: Implement comprehensive data anonymization, provide clear user consent mechanisms, and maintain audit trails for all location data access.
- Network reliability issues: Location updates may fail during poor connectivity, leading to gaps in tracking. Mitigation: Implement local buffering with retry logic and offline capability, and use connection status monitoring to trigger retransmissions.
- Scalability bottlenecks: High volume of concurrent location updates could overwhelm the system. Mitigation: Use horizontal sharding strategies, implement proper caching layers, and design for asynchronous processing to handle bursts in traffic.
- Battery drain on mobile devices: Frequent location updates can significantly impact device battery life. Mitigation: Implement adaptive sampling rates based on user behavior, use background processing efficiently, and provide user controls for tracking preferences.
Technical debt acknowledged
- Legacy data migration: Existing systems may have inconsistent location data formats that require careful migration strategies to maintain historical accuracy.
- Complexity of geospatial operations: Implementing efficient geospatial queries (distance calculations, polygon containment) introduces significant algorithmic complexity that may not be immediately obvious.
- Cross-platform compatibility: Supporting various mobile platforms (iOS, Android) with different location APIs and accuracy characteristics creates ongoing maintenance overhead.
- Eventual consistency handling: The complexity of managing and resolving location conflicts in a distributed system adds significant engineering overhead that may not be immediately apparent to stakeholders.
- Real-time analytics integration: Building real-time location analytics capabilities requires careful architectural planning to avoid performance degradation and ensure system stability.