Real-time Location Intelligence: Implementing Proximity Alerts in Java

Proximity alerts notify users or systems when a target enters or exits a defined geographical area. This capability is crucial for location-based services, security systems, logistics tracking, and real-time monitoring applications. Let's explore how to build robust proximity alert systems in Java.

Why Proximity Alerts in Java?

Key use cases:

  • Geofencing - Virtual boundaries for assets and people
  • Location-based Marketing - Notify customers near retail locations
  • Fleet Management - Monitor vehicle entry/exit from designated areas
  • Security Systems - Alert when assets move outside safe zones
  • Smart Cities - Traffic management and public safety notifications

Core Proximity Detection Engine

1. Geographical Calculations Service

@Service
@Slf4j
public class GeoCalculationService {
private static final double EARTH_RADIUS_METERS = 6371000; // Earth's radius in meters
/**
* Calculate distance between two points using Haversine formula
*/
public double calculateDistance(Location point1, Location point2) {
double lat1 = Math.toRadians(point1.getLatitude());
double lon1 = Math.toRadians(point1.getLongitude());
double lat2 = Math.toRadians(point2.getLatitude());
double lon2 = Math.toRadians(point2.getLongitude());
double dLat = lat2 - lat1;
double dLon = lon2 - lon1;
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS_METERS * c;
}
/**
* Check if point is within circular geofence
*/
public boolean isPointInCircle(Location point, Location center, double radiusMeters) {
double distance = calculateDistance(point, center);
return distance <= radiusMeters;
}
/**
* Check if point is within polygon using ray casting algorithm
*/
public boolean isPointInPolygon(Location point, List<Location> polygon) {
if (polygon.size() < 3) {
throw new IllegalArgumentException("Polygon must have at least 3 points");
}
boolean inside = false;
int n = polygon.size();
for (int i = 0, j = n - 1; i < n; j = i++) {
Location vertexI = polygon.get(i);
Location vertexJ = polygon.get(j);
if (((vertexI.getLatitude() > point.getLatitude()) != (vertexJ.getLatitude() > point.getLatitude())) &&
(point.getLongitude() < (vertexJ.getLongitude() - vertexI.getLongitude()) * 
(point.getLatitude() - vertexI.getLatitude()) / 
(vertexJ.getLatitude() - vertexI.getLatitude()) + vertexI.getLongitude())) {
inside = !inside;
}
}
return inside;
}
/**
* Check if point is within rectangle defined by northwest and southeast corners
*/
public boolean isPointInRectangle(Location point, Location northwest, Location southeast) {
return point.getLatitude() <= northwest.getLatitude() &&
point.getLatitude() >= southeast.getLatitude() &&
point.getLongitude() >= northwest.getLongitude() &&
point.getLongitude() <= southeast.getLongitude();
}
/**
* Calculate bearing between two points
*/
public double calculateBearing(Location start, Location end) {
double lat1 = Math.toRadians(start.getLatitude());
double lon1 = Math.toRadians(start.getLongitude());
double lat2 = Math.toRadians(end.getLatitude());
double lon2 = Math.toRadians(end.getLongitude());
double dLon = lon2 - lon1;
double y = Math.sin(dLon) * Math.cos(lat2);
double x = Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
double bearing = Math.toDegrees(Math.atan2(y, x));
return (bearing + 360) % 360; // Normalize to 0-360
}
/**
* Calculate destination point given distance and bearing
*/
public Location calculateDestination(Location start, double bearing, double distanceMeters) {
double lat1 = Math.toRadians(start.getLatitude());
double lon1 = Math.toRadians(start.getLongitude());
double bearingRad = Math.toRadians(bearing);
double angularDistance = distanceMeters / EARTH_RADIUS_METERS;
double lat2 = Math.asin(Math.sin(lat1) * Math.cos(angularDistance) +
Math.cos(lat1) * Math.sin(angularDistance) * Math.cos(bearingRad));
double lon2 = lon1 + Math.atan2(Math.sin(bearingRad) * Math.sin(angularDistance) * Math.cos(lat1),
Math.cos(angularDistance) - Math.sin(lat1) * Math.sin(lat2));
return new Location(Math.toDegrees(lat2), Math.toDegrees(lon2));
}
}

2. Geofence Management Service

@Service
@Slf4j
public class GeofenceService {
private final GeoCalculationService geoCalculator;
private final GeofenceRepository geofenceRepository;
private final Map<String, CachedGeofence> geofenceCache = new ConcurrentHashMap<>();
public GeofenceService(GeoCalculationService geoCalculator, GeofenceRepository geofenceRepository) {
this.geoCalculator = geoCalculator;
this.geofenceRepository = geofenceRepository;
loadGeofencesIntoCache();
}
public Geofence createGeofence(CreateGeofenceRequest request) {
Geofence geofence = Geofence.builder()
.id(UUID.randomUUID().toString())
.name(request.getName())
.description(request.getDescription())
.type(request.getType())
.coordinates(request.getCoordinates())
.radius(request.getRadius())
.isActive(true)
.createdAt(Instant.now())
.updatedAt(Instant.now())
.metadata(request.getMetadata())
.build();
Geofence saved = geofenceRepository.save(geofence);
cacheGeofence(saved);
log.info("Created geofence: {}", saved.getName());
return saved;
}
public List<GeofenceCheckResult> checkLocationAgainstGeofences(Location location, String deviceId) {
List<GeofenceCheckResult> results = new ArrayList<>();
for (CachedGeofence cachedGeofence : geofenceCache.values()) {
if (!cachedGeofence.isActive()) {
continue;
}
boolean isInside = checkPointInGeofence(location, cachedGeofence);
GeofenceCheckResult result = GeofenceCheckResult.builder()
.geofenceId(cachedGeofence.getId())
.geofenceName(cachedGeofence.getName())
.deviceId(deviceId)
.location(location)
.isInside(isInside)
.checkedAt(Instant.now())
.build();
results.add(result);
}
return results;
}
public GeofenceTransition checkGeofenceTransition(Location currentLocation, 
Location previousLocation, 
String deviceId) {
List<GeofenceCheckResult> currentResults = checkLocationAgainstGeofences(currentLocation, deviceId);
List<GeofenceCheckResult> previousResults = checkLocationAgainstGeofences(previousLocation, deviceId);
for (GeofenceCheckResult current : currentResults) {
for (GeofenceCheckResult previous : previousResults) {
if (current.getGeofenceId().equals(previous.getGeofenceId())) {
boolean wasInside = previous.isInside();
boolean isNowInside = current.isInside();
if (wasInside && !isNowInside) {
return GeofenceTransition.builder()
.type(TransitionType.EXIT)
.geofenceId(current.getGeofenceId())
.geofenceName(current.getGeofenceName())
.deviceId(deviceId)
.location(currentLocation)
.transitionTime(Instant.now())
.build();
} else if (!wasInside && isNowInside) {
return GeofenceTransition.builder()
.type(TransitionType.ENTER)
.geofenceId(current.getGeofenceId())
.geofenceName(current.getGeofenceName())
.deviceId(deviceId)
.location(currentLocation)
.transitionTime(Instant.now())
.build();
}
}
}
}
return null; // No transition detected
}
public List<Geofence> findGeofencesNearLocation(Location location, double radiusMeters) {
return geofenceCache.values().stream()
.filter(CachedGeofence::isActive)
.filter(geofence -> isGeofenceNearLocation(geofence, location, radiusMeters))
.map(this::mapToGeofence)
.collect(Collectors.toList());
}
private boolean checkPointInGeofence(Location location, CachedGeofence geofence) {
switch (geofence.getType()) {
case CIRCLE:
Location center = geofence.getCoordinates().get(0);
return geoCalculator.isPointInCircle(location, center, geofence.getRadius());
case POLYGON:
return geoCalculator.isPointInPolygon(location, geofence.getCoordinates());
case RECTANGLE:
if (geofence.getCoordinates().size() == 2) {
return geoCalculator.isPointInRectangle(location, 
geofence.getCoordinates().get(0), geofence.getCoordinates().get(1));
}
return false;
default:
log.warn("Unknown geofence type: {}", geofence.getType());
return false;
}
}
private boolean isGeofenceNearLocation(CachedGeofence geofence, Location location, double radiusMeters) {
// For circles, check distance to center
if (geofence.getType() == GeofenceType.CIRCLE) {
Location center = geofence.getCoordinates().get(0);
double distance = geoCalculator.calculateDistance(location, center);
return distance <= (radiusMeters + geofence.getRadius());
}
// For polygons and rectangles, check if any vertex is within radius
return geofence.getCoordinates().stream()
.anyMatch(vertex -> geoCalculator.calculateDistance(location, vertex) <= radiusMeters);
}
private void loadGeofencesIntoCache() {
List<Geofence> activeGeofences = geofenceRepository.findByIsActiveTrue();
activeGeofences.forEach(this::cacheGeofence);
log.info("Loaded {} geofences into cache", activeGeofences.size());
}
private void cacheGeofence(Geofence geofence) {
geofenceCache.put(geofence.getId(), mapToCachedGeofence(geofence));
}
private CachedGeofence mapToCachedGeofence(Geofence geofence) {
return CachedGeofence.builder()
.id(geofence.getId())
.name(geofence.getName())
.type(geofence.getType())
.coordinates(geofence.getCoordinates())
.radius(geofence.getRadius())
.isActive(geofence.isActive())
.metadata(geofence.getMetadata())
.build();
}
private Geofence mapToGeofence(CachedGeofence cached) {
return Geofence.builder()
.id(cached.getId())
.name(cached.getName())
.type(cached.getType())
.coordinates(cached.getCoordinates())
.radius(cached.getRadius())
.isActive(cached.isActive())
.metadata(cached.getMetadata())
.build();
}
}

Proximity Alert Engine

1. Real-time Alert Service

@Service
@Slf4j
public class ProximityAlertService {
private final GeofenceService geofenceService;
private final AlertNotificationService notificationService;
private final AlertRepository alertRepository;
private final DeviceTrackingService deviceTrackingService;
// Track device states to detect transitions
private final Map<String, DeviceGeofenceState> deviceStates = new ConcurrentHashMap<>();
public ProximityAlertService(GeofenceService geofenceService,
AlertNotificationService notificationService,
AlertRepository alertRepository,
DeviceTrackingService deviceTrackingService) {
this.geofenceService = geofenceService;
this.notificationService = notificationService;
this.alertRepository = alertRepository;
this.deviceTrackingService = deviceTrackingService;
}
@Async
public void processLocationUpdate(LocationUpdate update) {
try {
String deviceId = update.getDeviceId();
Location currentLocation = update.getLocation();
// Get previous location for transition detection
Location previousLocation = deviceTrackingService.getLastKnownLocation(deviceId);
if (previousLocation != null) {
// Check for geofence transitions
GeofenceTransition transition = geofenceService.checkGeofenceTransition(
currentLocation, previousLocation, deviceId);
if (transition != null) {
handleGeofenceTransition(transition, update);
}
}
// Check for proximity to points of interest
checkProximityToPointsOfInterest(currentLocation, deviceId, update.getTimestamp());
// Update device tracking
deviceTrackingService.updateDeviceLocation(deviceId, currentLocation, update.getTimestamp());
} catch (Exception e) {
log.error("Failed to process location update for device: {}", update.getDeviceId(), e);
}
}
public void checkProximityToTarget(Location deviceLocation, Location targetLocation, 
String deviceId, String targetId, double alertRadius) {
double distance = geoCalculator.calculateDistance(deviceLocation, targetLocation);
if (distance <= alertRadius) {
ProximityAlert alert = ProximityAlert.builder()
.id(UUID.randomUUID().toString())
.deviceId(deviceId)
.targetId(targetId)
.targetType("LOCATION")
.distance(distance)
.deviceLocation(deviceLocation)
.targetLocation(targetLocation)
.alertRadius(alertRadius)
.triggeredAt(Instant.now())
.build();
triggerProximityAlert(alert);
}
}
public void monitorDeviceProximity(String deviceId, String targetDeviceId, double alertRadius) {
Location deviceLocation = deviceTrackingService.getLastKnownLocation(deviceId);
Location targetLocation = deviceTrackingService.getLastKnownLocation(targetDeviceId);
if (deviceLocation != null && targetLocation != null) {
checkProximityToTarget(deviceLocation, targetLocation, deviceId, targetDeviceId, alertRadius);
}
}
@Scheduled(fixedRate = 30000) // Check every 30 seconds
public void performScheduledProximityChecks() {
log.info("Performing scheduled proximity checks");
// Check all active devices against all geofences
Map<String, Location> activeDevices = deviceTrackingService.getActiveDevices();
activeDevices.forEach((deviceId, location) -> {
try {
List<GeofenceCheckResult> results = geofenceService.checkLocationAgainstGeofences(location, deviceId);
results.forEach(this::processGeofenceCheckResult);
} catch (Exception e) {
log.error("Failed to check proximity for device: {}", deviceId, e);
}
});
}
private void handleGeofenceTransition(GeofenceTransition transition, LocationUpdate update) {
log.info("Geofence {} detected: device {} {}", 
transition.getGeofenceName(), transition.getDeviceId(), transition.getType());
// Create alert
GeofenceAlert alert = GeofenceAlert.builder()
.id(UUID.randomUUID().toString())
.deviceId(transition.getDeviceId())
.geofenceId(transition.getGeofenceId())
.geofenceName(transition.getGeofenceName())
.transitionType(transition.getType())
.location(transition.getLocation())
.triggeredAt(transition.getTransitionTime())
.metadata(update.getMetadata())
.build();
// Save alert
alertRepository.save(alert);
// Send notifications
notificationService.sendGeofenceAlert(alert);
// Update device state
updateDeviceGeofenceState(transition.getDeviceId(), transition.getGeofenceId(), 
transition.getType() == TransitionType.ENTER);
}
private void checkProximityToPointsOfInterest(Location location, String deviceId, Instant timestamp) {
// Implementation would check against predefined points of interest
// This is a simplified example
List<PointOfInterest> nearbyPOIs = pointOfInterestService.findNearby(location, 1000); // 1km radius
for (PointOfInterest poi : nearbyPOIs) {
double distance = geoCalculator.calculateDistance(location, poi.getLocation());
if (distance <= poi.getNotificationRadius()) {
triggerPointOfInterestAlert(deviceId, poi, distance, timestamp);
}
}
}
private void processGeofenceCheckResult(GeofenceCheckResult result) {
// Update device state and detect state changes
DeviceGeofenceState deviceState = deviceStates.computeIfAbsent(
result.getDeviceId(), id -> new DeviceGeofenceState(id));
boolean stateChanged = deviceState.updateGeofenceState(
result.getGeofenceId(), result.isInside(), result.getCheckedAt());
if (stateChanged) {
// Handle state change (e.g., send notification for continuous monitoring)
log.debug("Geofence state changed for device {} in geofence {}", 
result.getDeviceId(), result.getGeofenceName());
}
}
private void triggerProximityAlert(ProximityAlert alert) {
alertRepository.save(alert);
notificationService.sendProximityAlert(alert);
log.info("Proximity alert triggered: device {} is {}m from target {}", 
alert.getDeviceId(), alert.getDistance(), alert.getTargetId());
}
private void triggerPointOfInterestAlert(String deviceId, PointOfInterest poi, 
double distance, Instant timestamp) {
POIProximityAlert alert = POIProximityAlert.builder()
.id(UUID.randomUUID().toString())
.deviceId(deviceId)
.poiId(poi.getId())
.poiName(poi.getName())
.poiType(poi.getType())
.distance(distance)
.triggeredAt(timestamp)
.build();
alertRepository.save(alert);
notificationService.sendPOIProximityAlert(alert);
}
private void updateDeviceGeofenceState(String deviceId, String geofenceId, boolean isInside) {
DeviceGeofenceState state = deviceStates.computeIfAbsent(
deviceId, id -> new DeviceGeofenceState(id));
state.updateGeofenceState(geofenceId, isInside, Instant.now());
}
}

Notification Service

1. Multi-channel Alert Notification

@Service
@Slf4j
public class AlertNotificationService {
private final EmailService emailService;
private final SMSService smsService;
private final PushNotificationService pushService;
private final WebSocketService webSocketService;
private final AlertTemplateService templateService;
public AlertNotificationService(EmailService emailService, SMSService smsService,
PushNotificationService pushService, 
WebSocketService webSocketService,
AlertTemplateService templateService) {
this.emailService = emailService;
this.smsService = smsService;
this.pushService = pushService;
this.webSocketService = webSocketService;
this.templateService = templateService;
}
public void sendGeofenceAlert(GeofenceAlert alert) {
// Get notification preferences for the device/user
NotificationPreferences preferences = getNotificationPreferences(alert.getDeviceId());
// Prepare notification message
String message = templateService.generateGeofenceAlertMessage(alert);
String subject = templateService.generateGeofenceAlertSubject(alert);
// Send via configured channels
if (preferences.isEmailEnabled()) {
sendEmailAlert(alert, subject, message, preferences.getEmailRecipients());
}
if (preferences.isSmsEnabled()) {
sendSMSAlert(alert, message, preferences.getPhoneNumbers());
}
if (preferences.isPushEnabled()) {
sendPushAlert(alert, message, preferences.getDeviceTokens());
}
// Always send to WebSocket for real-time dashboard updates
sendWebSocketAlert(alert, message);
log.info("Sent geofence alert notifications for device: {}", alert.getDeviceId());
}
public void sendProximityAlert(ProximityAlert alert) {
NotificationPreferences preferences = getNotificationPreferences(alert.getDeviceId());
String message = templateService.generateProximityAlertMessage(alert);
if (preferences.isPushEnabled()) {
sendPushAlert(alert, message, preferences.getDeviceTokens());
}
sendWebSocketAlert(alert, message);
log.debug("Sent proximity alert for device: {}", alert.getDeviceId());
}
public void sendPOIProximityAlert(POIProximityAlert alert) {
// For POI alerts, we might want different notification rules
String message = templateService.generatePOIAlertMessage(alert);
sendPushAlert(alert, message, getDeviceTokens(alert.getDeviceId()));
sendWebSocketAlert(alert, message);
}
public void sendBulkAlerts(List<Alert> alerts) {
// Group alerts by device for efficient notification
Map<String, List<Alert>> alertsByDevice = alerts.stream()
.collect(Collectors.groupingBy(Alert::getDeviceId));
alertsByDevice.forEach((deviceId, deviceAlerts) -> {
if (deviceAlerts.size() > 1) {
// Send consolidated alert for multiple events
sendConsolidatedAlert(deviceId, deviceAlerts);
} else {
// Send individual alert
sendGeofenceAlert((GeofenceAlert) deviceAlerts.get(0));
}
});
}
private void sendEmailAlert(Alert alert, String subject, String message, List<String> recipients) {
try {
EmailRequest emailRequest = EmailRequest.builder()
.to(recipients)
.subject(subject)
.body(message)
.template("geofence-alert")
.variables(Map.of("alert", alert))
.build();
emailService.sendEmail(emailRequest);
} catch (Exception e) {
log.error("Failed to send email alert for device: {}", alert.getDeviceId(), e);
}
}
private void sendSMSAlert(Alert alert, String message, List<String> phoneNumbers) {
for (String phoneNumber : phoneNumbers) {
try {
SMSRequest smsRequest = SMSRequest.builder()
.to(phoneNumber)
.message(message)
.build();
smsService.sendSMS(smsRequest);
} catch (Exception e) {
log.error("Failed to send SMS alert to: {}", phoneNumber, e);
}
}
}
private void sendPushAlert(Alert alert, String message, List<String> deviceTokens) {
for (String deviceToken : deviceTokens) {
try {
PushNotificationRequest pushRequest = PushNotificationRequest.builder()
.deviceToken(deviceToken)
.title("Proximity Alert")
.body(message)
.data(Map.of(
"alertId", alert.getId(),
"type", alert.getType(),
"timestamp", alert.getTriggeredAt().toString()
))
.build();
pushService.sendNotification(pushRequest);
} catch (Exception e) {
log.error("Failed to send push alert to device token: {}", deviceToken, e);
}
}
}
private void sendWebSocketAlert(Alert alert, String message) {
try {
WebSocketMessage wsMessage = WebSocketMessage.builder()
.type("PROXIMITY_ALERT")
.payload(Map.of(
"alert", alert,
"message", message,
"timestamp", Instant.now().toString()
))
.build();
webSocketService.sendToUser(alert.getDeviceId(), wsMessage);
} catch (Exception e) {
log.error("Failed to send WebSocket alert for device: {}", alert.getDeviceId(), e);
}
}
private void sendConsolidatedAlert(String deviceId, List<Alert> alerts) {
// Implementation for consolidated alerts (multiple events in one notification)
String consolidatedMessage = templateService.generateConsolidatedAlertMessage(alerts);
NotificationPreferences preferences = getNotificationPreferences(deviceId);
if (preferences.isPushEnabled()) {
// Send consolidated push notification
}
log.info("Sent consolidated alert for {} events to device: {}", alerts.size(), deviceId);
}
private NotificationPreferences getNotificationPreferences(String deviceId) {
// In practice, this would fetch from database or user preferences service
return NotificationPreferences.builder()
.emailEnabled(true)
.smsEnabled(false)
.pushEnabled(true)
.emailRecipients(List.of("[email protected]"))
.phoneNumbers(List.of())
.deviceTokens(List.of("device-token-" + deviceId))
.build();
}
private List<String> getDeviceTokens(String deviceId) {
// Implementation would fetch from device registry
return List.of("device-token-" + deviceId);
}
}

Data Models

1. Core Domain Models

@Data
@Builder
public class Location {
private Double latitude;
private Double longitude;
private Double altitude;
private Float accuracy;
private Float speed;
private Float bearing;
private Instant timestamp;
public Location() {}
public Location(Double latitude, Double longitude) {
this.latitude = latitude;
this.longitude = longitude;
this.timestamp = Instant.now();
}
}
@Data
@Builder
public class Geofence {
private String id;
private String name;
private String description;
private GeofenceType type;
private List<Location> coordinates;
private Double radius; // For circular geofences
private boolean isActive;
private Instant createdAt;
private Instant updatedAt;
private Map<String, Object> metadata;
}
@Data
@Builder
public class GeofenceCheckResult {
private String geofenceId;
private String geofenceName;
private String deviceId;
private Location location;
private boolean isInside;
private Instant checkedAt;
}
@Data
@Builder
public class GeofenceTransition {
private TransitionType type;
private String geofenceId;
private String geofenceName;
private String deviceId;
private Location location;
private Instant transitionTime;
}
@Data
@Builder
public abstract class Alert {
private String id;
private String deviceId;
private AlertType type;
private Location location;
private Instant triggeredAt;
private Map<String, Object> metadata;
}
@Data
@Builder
public class GeofenceAlert extends Alert {
private String geofenceId;
private String geofenceName;
private TransitionType transitionType;
}
@Data
@Builder
public class ProximityAlert extends Alert {
private String targetId;
private String targetType;
private Location targetLocation;
private Double distance;
private Double alertRadius;
}
@Data
@Builder
public class LocationUpdate {
private String deviceId;
private Location location;
private Instant timestamp;
private Map<String, Object> metadata;
}
// Enums
public enum GeofenceType {
CIRCLE, POLYGON, RECTANGLE
}
public enum TransitionType {
ENTER, EXIT, DWELL, LEFT
}
public enum AlertType {
GEOFENCE_ENTER, GEOFENCE_EXIT, PROXIMITY, POI_NEARBY, FENCE_VIOLATION
}
// Supporting classes
@Data
@Builder
public class CachedGeofence {
private String id;
private String name;
private GeofenceType type;
private List<Location> coordinates;
private Double radius;
private boolean isActive;
private Map<String, Object> metadata;
}
@Data
public class DeviceGeofenceState {
private String deviceId;
private Map<String, Boolean> geofenceStates; // geofenceId -> isInside
private Instant lastUpdate;
public DeviceGeofenceState(String deviceId) {
this.deviceId = deviceId;
this.geofenceStates = new HashMap<>();
this.lastUpdate = Instant.now();
}
public boolean updateGeofenceState(String geofenceId, boolean isInside, Instant timestamp) {
Boolean previousState = geofenceStates.put(geofenceId, isInside);
this.lastUpdate = timestamp;
return previousState == null || previousState != isInside;
}
public boolean isInsideGeofence(String geofenceId) {
return geofenceStates.getOrDefault(geofenceId, false);
}
}
@Data
@Builder
public class NotificationPreferences {
private boolean emailEnabled;
private boolean smsEnabled;
private boolean pushEnabled;
private List<String> emailRecipients;
private List<String> phoneNumbers;
private List<String> deviceTokens;
}

REST Controller

1. Proximity Alert API

@RestController
@RequestMapping("/api/proximity")
@Slf4j
public class ProximityAlertController {
private final ProximityAlertService alertService;
private final GeofenceService geofenceService;
private final AlertRepository alertRepository;
public ProximityAlertController(ProximityAlertService alertService,
GeofenceService geofenceService,
AlertRepository alertRepository) {
this.alertService = alertService;
this.geofenceService = geofenceService;
this.alertRepository = alertRepository;
}
@PostMapping("/location-update")
public ResponseEntity<Void> processLocationUpdate(@RequestBody LocationUpdate update) {
try {
alertService.processLocationUpdate(update);
return ResponseEntity.accepted().build();
} catch (Exception e) {
log.error("Failed to process location update", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/geofences")
public ResponseEntity<Geofence> createGeofence(@RequestBody CreateGeofenceRequest request) {
try {
Geofence geofence = geofenceService.createGeofence(request);
return ResponseEntity.status(HttpStatus.CREATED).body(geofence);
} catch (Exception e) {
log.error("Failed to create geofence", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
@PostMapping("/check-proximity")
public ResponseEntity<ProximityCheckResponse> checkProximity(@RequestBody ProximityCheckRequest request) {
try {
List<GeofenceCheckResult> results = geofenceService.checkLocationAgainstGeofences(
request.getLocation(), request.getDeviceId());
ProximityCheckResponse response = ProximityCheckResponse.builder()
.deviceId(request.getDeviceId())
.location(request.getLocation())
.checkedAt(Instant.now())
.results(results)
.build();
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to check proximity", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
@GetMapping("/alerts/{deviceId}")
public ResponseEntity<List<Alert>> getDeviceAlerts(
@PathVariable String deviceId,
@RequestParam(defaultValue = "24") Integer hours) {
try {
Instant since = Instant.now().minus(hours, ChronoUnit.HOURS);
List<Alert> alerts = alertRepository.findByDeviceIdAndTriggeredAtAfter(deviceId, since);
return ResponseEntity.ok(alerts);
} catch (Exception e) {
log.error("Failed to get alerts for device: {}", deviceId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/monitor-device-proximity")
public ResponseEntity<Void> monitorDeviceProximity(@RequestBody DeviceProximityRequest request) {
try {
alertService.monitorDeviceProximity(
request.getDeviceId(), 
request.getTargetDeviceId(), 
request.getAlertRadius()
);
return ResponseEntity.accepted().build();
} catch (Exception e) {
log.error("Failed to setup device proximity monitoring", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
@GetMapping("/geofences/nearby")
public ResponseEntity<List<Geofence>> findNearbyGeofences(
@RequestParam Double lat, @RequestParam Double lng,
@RequestParam(defaultValue = "1000") Double radiusMeters) {
try {
Location location = new Location(lat, lng);
List<Geofence> geofences = geofenceService.findGeofencesNearLocation(location, radiusMeters);
return ResponseEntity.ok(geofences);
} catch (Exception e) {
log.error("Failed to find nearby geofences", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
// Request DTOs
@Data
public static class CreateGeofenceRequest {
private String name;
private String description;
private GeofenceType type;
private List<Location> coordinates;
private Double radius;
private Map<String, Object> metadata;
}
@Data
public static class ProximityCheckRequest {
private String deviceId;
private Location location;
}
@Data
@Builder
public static class ProximityCheckResponse {
private String deviceId;
private Location location;
private Instant checkedAt;
private List<GeofenceCheckResult> results;
}
@Data
public static class DeviceProximityRequest {
private String deviceId;
private String targetDeviceId;
private Double alertRadius;
}
}

Configuration

1. Application Properties

# application.yml
proximity:
alert:
enabled: true
check-interval: 30000
max-geofence-radius: 50000
notification:
email-enabled: true
sms-enabled: false
push-enabled: true
geofence:
cache-enabled: true
cache-refresh-interval: 300000
spring:
task:
execution:
pool:
core-size: 10
max-size: 50
queue-capacity: 1000

Best Practices

  1. Performance Optimization - Use spatial indexes for geofence queries
  2. Scalability - Implement distributed caching for device states
  3. Accuracy - Consider using more precise geographical calculations for critical applications
  4. Battery Optimization - Adjust location update frequency based on use case
  5. Error Handling - Implement robust error handling for location services
  6. Privacy - Ensure compliance with data protection regulations
  7. Monitoring - Track alert volumes and system performance

Conclusion

Implementing proximity alerts in Java enables powerful location-aware applications across various domains. By leveraging the patterns shown here, developers can:

  • Create Virtual Boundaries - Implement geofencing for security and monitoring
  • Enable Real-time Notifications - Alert users based on location changes
  • Optimize Operations - Monitor asset movements and fleet locations
  • Enhance User Experience - Provide context-aware services
  • Ensure Safety - Monitor restricted areas and safety zones

The combination of geographical calculations, real-time processing, and multi-channel notifications creates a comprehensive proximity alert system that can scale from simple location tracking to complex enterprise monitoring solutions.

Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)

https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.

https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.

https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.

https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.

https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)

https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.

https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.

https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.

https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.


Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper