TagServer Integration

Overview

TagServer is an in-house developed timing system used for race events. The EMS synchronises event, participant, and timing data to TagServer to enable:

  • Timing point configuration

  • Participant lookup by bib number or RFID tag

  • Real-time results capture

  • Start group management

Entities Synced to TagServer

The following 8 entity types are synchronised to TagServer:

Main Entity Sync Entity Purpose

Event

EventSyncTagServer

Event details (name, dates)

User (Person)

PersonSyncTagServer

Participant personal information

Organisation

OrganisationSyncTagServer

Organiser information

Race

RaceSyncTagServer

Race configuration within an event

StartGroup

StartGroupSyncTagServer

Start wave/group definitions

StartGroupParticipant

StartGroupParticipantSyncTagServer

Participant-to-start-group assignments

EventParticipant

EventParticipantSyncTagServer

Event entry details (bib, payment status)

Tag (BibTagMapping)

BibTagMappingSyncTagServer

RFID tag to participant mapping

Architecture

Package Structure

admin-service/src/main/java/za/co/idealogic/event/admin/service/sync/tagserver/
├── BaseSyncService.java           # Generic sync orchestration
├── SyncController.java            # Scheduler and coordinator
├── ApiApiImpl.java                # REST API client (RestTemplate)
├── ApiConfig.java                 # API client configuration
├── AuthRemoteLoginService.java    # Authentication handling
├── AuthTokenInterceptor.java      # JWT token injection
│
├── EventSyncService.java          # Event sync implementation
├── PersonSyncService.java         # Person sync implementation
├── OrganisationSyncService.java   # Organisation sync implementation
├── RaceSyncService.java           # Race sync implementation
├── StartGroupSyncService.java     # StartGroup sync implementation
├── StartGroupParticipantSyncService.java
├── EventParticipantSyncService.java
└── BibTagMappingSyncService.java
database/src/main/java/za/co/idealogic/event/
├── domain/
│   ├── BaseSyncEntity.java        # Abstract sync entity base
│   ├── EventSyncTagServer.java
│   ├── PersonSyncTagServer.java
│   ├── OrganisationSyncTagServer.java
│   ├── RaceSyncTagServer.java
│   ├── StartGroupSyncTagServer.java
│   ├── StartGroupParticipantSyncTagServer.java
│   ├── EventParticipantSyncTagServer.java
│   └── BibTagMappingSyncTagServer.java
│
└── repository/
    ├── BaseSyncEntityRepository.java
    ├── EventSyncTagServerRepository.java
    ├── PersonSyncTagServerRepository.java
    └── ... (one per entity)

Class Relationships

┌─────────────────────────────┐
│     SyncController          │  Coordinates all sync services
│  - scheduledSync()          │  Runs on cron schedule
│  - addService()             │
└──────────┬──────────────────┘
           │ manages
           ▼
┌─────────────────────────────┐
│   BaseSyncService<E, S>     │  Generic sync orchestration
│  - syncToRemote()           │
│  - syncToMain()             │  ← Not implemented
│  - handleDeletions()        │
│  - processEntity()          │
└──────────┬──────────────────┘
           │ extends
           ▼
┌─────────────────────────────┐
│   EventSyncService          │  Entity-specific implementation
│  - createInRemote()         │
│  - updateInRemote()         │
│  - deleteFromRemote()       │
│  - convertToDTO()           │
│  - calculateRemoteHash()    │
└─────────────────────────────┘

BaseSyncEntity

The base class for all sync entities:

@MappedSuperclass
public abstract class BaseSyncEntity<E> {

    private Instant lastModifiedMain;    // When synced TO remote
    private Instant lastModifiedRemote;  // When synced FROM remote

    public abstract E getMainEntity();
    public abstract void setMainEntity(E entity);
    public abstract int getDataHash(E entity);
}
The current implementation uses lastModifiedMain and lastModifiedRemote instead of entitySyncedOn and remoteSyncedOn from the spec.

Hash Calculation

Each sync entity implements getDataHash() to calculate a content hash for change detection.

Event

Objects.hash(
    entity.getId(),
    entity.getName(),
    entity.getStartDateTime(),
    entity.getEndDateTime()
)

Person

PersonWrapper wrapper = new PersonWrapper(entity);
Objects.hash(
    wrapper.getFirstName(),
    wrapper.getLastName(),
    wrapper.getContactNumber(),
    wrapper.getEmail()
)

Organisation

Objects.hash(
    entity.getId(),
    entity.getName(),
    entity.getEmail(),
    entity.getContactNumber()
)

Race

Objects.hash(
    entity.getId(),
    entity.getName(),
    entity.getDistanceInMeters()
)

StartGroup

Objects.hash(
    entity.getId(),
    entity.getName()  // Derived from race name + sequence
)

StartGroupParticipant

Objects.hash(
    entity.getId()
)

EventParticipant

Objects.hash(
    entity.getId(),
    entity.getPaid()
)

BibTagMapping (Tag)

Objects.hash(
    entity.getId(),
    entity.getPerson() != null ? entity.getPerson().getId() : null
)

API Client

The current implementation uses RestTemplate via ApiApiImpl:

Operation Method

Create Event

POST /api/events

Update Event

PATCH /api/events/{id}

Get Event

GET /api/events/{id}

Delete Event

DELETE /api/events/{id}

Similar patterns for other entities

Authentication

  • AuthRemoteLoginService performs initial login on startup

  • JWT token stored and provided via AuthTokenInterceptor

  • X-Org-Id header added for organisation-scoped requests

Current Configuration

The following values are currently hardcoded and should be externalised.
// AuthRemoteLoginService.java
username = "admin";
password = "admin";

// ApiApiImpl.java
baseUrl = "http://localhost:12510";

Sync Service Implementation

Each entity has a corresponding sync service extending BaseSyncService:

@Service
public class EventSyncService extends BaseSyncService<Event, EventSyncTagServer> {

    @Override
    protected void createInRemote(Event entity, EventSyncTagServer syncEntity) {
        EventDTOApiDTO dto = (EventDTOApiDTO) convertToDTO(entity);
        String xOrgId = getOrgId(entity);
        apiClient.createEvent(dto, xOrgId);
    }

    @Override
    protected void updateInRemote(Event entity, EventSyncTagServer syncEntity) {
        EventDTOApiDTO dto = (EventDTOApiDTO) convertToDTO(entity);
        apiClient.partialUpdateEvent(entity.getId(), dto, getOrgId(entity));
    }

    @Override
    protected void deleteFromRemote(EventSyncTagServer syncEntity) {
        Event entity = syncEntity.getMainEntity();
        apiClient.deleteEvent(entity.getId(), getOrgId(entity));
    }

    @Override
    public void syncToMain() {
        log.info("Remote changes to main not implemented yet for events");
    }
}

Scheduling

The SyncController manages scheduling:

@Scheduled(cron = "${sync.cron.expression:0 */1 * * * *}")
public void scheduledSync() {
    syncAllToRemote();
    // syncAllToMain();  // Currently disabled
}

Default: Every minute (0 */1 * * * *)

Implementation Status

Feature Status Details

Outbound sync

Implemented

CREATE, UPDATE, DELETE for all 8 entities

Inbound sync

Not implemented

All syncToMain() methods are stubs

Deletion detection

Implemented

Orphaned sync records detected and remote entities deleted

Hash comparison

Implemented

Local vs remote hash comparison determines UPDATE vs SKIP

Per-entity transactions

Implemented

@Transactional(propagation = REQUIRES_NEW) prevents batch rollback

Error handling

Basic

Exceptions logged but no retry mechanism

Authentication

Implemented

JWT token-based with interceptor

Known Issues and Gaps

Critical

  1. No inbound sync - All syncToMain() methods are stubs. Changes made in TagServer are not synchronised back to EMS.

  2. Missing remoteId field - Cannot track TagServer’s entity IDs. Currently assumes IDs are identical on both systems.

  3. Hardcoded credentials - Username, password, and base URL are in source code.

Important

  1. No retry mechanism - Failed syncs are logged but not retried. Network issues can cause permanent desynchronisation.

  2. No bounce-back detection - While the infrastructure exists, inbound sync is not implemented, so bounce-backs cannot be detected.

  3. Missing audit log - No record of sync operations for troubleshooting.

Minor

  1. No health check endpoint - No way to monitor sync status programmatically.

  2. Fixed sync schedule - Cron expression not easily configurable per environment.

Database Tables

Sync tables are created via Liquibase changelog:

20251027173329_add_sync_Entities.xml
<createTable tableName="event_sync_tag_server">
    <column name="id" type="bigint" autoIncrement="true">
        <constraints primaryKey="true"/>
    </column>
    <column name="event_id" type="bigint">
        <constraints nullable="false"/>
    </column>
    <column name="last_modified_main" type="timestamp"/>
    <column name="last_modified_remote" type="timestamp"/>
</createTable>

Similar structure for all 8 sync entity tables.

Source Code References

Table 1. Admin Service (feature/sync branch)
File Purpose

BaseSyncService.java

Generic sync orchestration (~344 lines)

SyncController.java

Scheduler and service registry

ApiApiImpl.java

REST client implementation (~217 lines, 25 methods)

AuthRemoteLoginService.java

JWT authentication

*SyncService.java

Entity-specific sync implementations

Table 2. Database Module (feature/sync branch)
File Purpose

BaseSyncEntity.java

Abstract sync entity base (~30 lines)

BaseSyncEntityRepository.java

Common repository queries

*SyncTagServer.java

Entity-specific sync tables

20251027173329_add_sync_Entities.xml

Liquibase changelog