Skip to main content

Location Tracking System Design

Comprehensive low-level design for a scalable location tracking model with real-time updates, storage optimization, and privacy considerations.

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

  1. 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+)?
  2. 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?
  3. 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?
  4. 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?
  5. 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?
  6. 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?
  7. 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

MethodPathDescriptionAuthRate Limit
POST/v1/locationsReport current location for a deviceJWT + Device Auth100 req/min
GET/v1/locations/{deviceId}Get latest location for deviceJWT + Device Auth50 req/min
GET/v1/locations/{deviceId}/historyGet location history for deviceJWT + Device Auth20 req/min
POST/v1/locations/streamSubscribe to real-time location updatesJWT + Device Auth5 req/min
DELETE/v1/locations/{deviceId}Delete all location data for deviceJWT + Device Auth5 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 CodeDescription
200Success
201Created
400Bad Request - Invalid input
401Unauthorized - Invalid auth token
403Forbidden - Insufficient permissions
429Too Many Requests - Rate limit exceeded
500Internal 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:

  1. In-memory token bucket per device
  2. Redis distributed rate limiting for multi-instance deployments
  3. Sliding window algorithm to prevent burst attacks

Idempotency Design

Idempotency key strategy:

  • Client provides X-Idempotency-Key header 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 NameColumnsTypePurpose
idx_users_emailemailStandardFast user lookup by email
idx_locations_user_iduser_idStandardEfficient location queries per user
idx_locations_recorded_atrecorded_atStandardTime-range queries for location history
idx_locations_user_recordeduser_id, recorded_atCompositeFastest queries for recent locations
idx_locations_created_atcreated_atStandardAudit 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_id hash 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_at timestamps
  • user_id for 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:

  1. LocationService - Main entry point for location tracking operations
  2. LocationProcessorImpl - Core business logic processor handling location validation, geofence detection, and event publishing
  3. GeofenceService - Geofence management and alerting logic
  4. 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:

  1. Single Location Tracking Operation - Complete atomic operation including validation, storage, and alert generation
  2. 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:

  1. Saga Coordinator - Manages the state of distributed operations
  2. Compensating Actions - Rollback mechanisms for failed steps
  3. 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:

  1. Immutable location objects - all location data is immutable once created
  2. Atomic operations for timestamp-based ordering and conflict resolution
  3. Optimistic locking using version numbers in the database for concurrent updates
  4. 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:

  1. Fixed lock ordering - all database operations follow consistent ordering (by entity ID, then timestamp)
  2. Timeout-based lock acquisition - 500ms timeout for database locks
  3. Hierarchical resource management - location entity locks acquired before session locks
  4. Transaction isolation level set to READ_COMMITTED to prevent phantom reads

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency will be handled through:

  1. Client-generated unique request IDs (UUIDs) sent with each location update
  2. Database-level deduplication using unique constraints on (client_id, request_id)
  3. In-memory cache of recent request IDs with TTL of 5 minutes for fast rejection
  4. 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:

  1. Batch processing of location updates to reduce database load
  2. Async processing with bounded thread pools (max 100 threads)
  3. Rate limiting per device to prevent abuse
  4. 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 TypeTTLReason
Recent locations5 minutesHigh frequency updates, recent data is most valuable
Historical locations30 daysCompliance and analytics requirements
User profile cache1 hourStable user data, infrequent changes
Device metadata24 hoursDevice 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

  1. Probabilistic Early Expiry: 10% of cache entries expire 2 minutes early
  2. Distributed Locking: For hot keys, use Redis distributed locks during cache rebuilds
  3. 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-lru with 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:

  1. Performance: Raw queries avoid unnecessary object mapping overhead
  2. Complex aggregations: Location data often requires time-series analytics that are better expressed with SQL
  3. 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:

  1. Domain Errors - Business logic violations (e.g., invalid location data, exceeded rate limits)
  2. Infrastructure Errors - System-level issues (e.g., database connection failures, cache unavailability)
  3. 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 CodeSeverityAlert ThresholdDescription
INVALID_LOCATION_DATAWARN100 per hourValidation errors
LOCATION_SERVICE_UNAVAILABLECRITICAL5 per minuteService outages
RATE_LIMIT_EXCEEDEDINFO1000 per hourUser 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:

  1. User authenticates via OAuth2 or username/password
  2. System generates JWT with user ID, role, and expiration
  3. Middleware validates token on every request
  4. 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:

  1. location.track

    • Attributes: user_id, request_id, latitude, longitude, accuracy
    • Custom attributes: location_update_type (REALTIME, BATCH), device_id
  2. database.location.save

    • Attributes: db.operation, db.statement, db.connection_id
    • Custom attributes: retry_count, is_retry
  3. cache.location.get

    • Attributes: cache.hit, cache.key
    • Custom attributes: cache.ttl_seconds

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 NameTypeLabelsDescription
location_updates_totalCounterstatus, user_id, update_typeTotal location updates processed
location_update_errors_totalCountererror_code, user_idErrors during location updates
location_update_duration_secondsHistogramstatus, user_idDuration of location update operations
location_cache_hits_totalCountercache_type, user_idCache hits for location data
location_cache_misses_totalCountercache_type, user_idCache misses for location data
location_database_queries_totalCounteroperation, statusDatabase query counts
location_database_query_duration_secondsHistogramoperation, statusDatabase query execution times

Additional Gauge Metrics:

  • location_active_users (Gauge): Current number of users with active location tracking
  • location_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

MetricAlert LevelThresholdFrequencyAction
location_update_errors_totalWarning> 5 errors/minuteEvery minuteSlack alert
location_update_errors_totalCritical> 50 errors/minuteEvery minuteOn-call escalation
location_update_duration_secondsWarning> 500ms p95Every 5 minutesSlack alert
location_update_duration_secondsCritical> 2000ms p95Every 5 minutesOn-call escalation
location_database_query_duration_secondsWarning> 1000ms p95Every 5 minutesSlack alert
location_database_query_duration_secondsCritical> 3000ms p95Every 5 minutesOn-call escalation
location_cache_misses_totalWarning> 1000 misses/minuteEvery minuteSlack alert
location_cache_misses_totalCritical> 5000 misses/minuteEvery minuteOn-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-offOption AOption BJustification
Storage vs. Query PerformanceStore raw GPS points with minimal processingPre-aggregate and store location clustersRaw points provide maximum flexibility for analytics but increase storage costs and query complexity. Aggregation reduces storage but limits analytical capabilities.
Consistency vs. AvailabilityStrong consistency for location updatesEventual consistency with optimistic conflict resolutionStrong consistency ensures data accuracy but may impact availability during network partitions. Eventual consistency improves availability but requires conflict resolution logic.
Precision vs. Battery LifeHigh-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 TrackingRely solely on GPS for location dataCombine GPS, WiFi, cellular, and sensor dataSingle 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.

Was this article helpful?

Topics in this article

More from Backend LLD

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.