Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Train Search and Ticket Availability Queries in a Ticketing System

Tech 1

The train data search endpoint is implemented via the TicketController, with the request handler routing to the appropriate service method.

@GetMapping("/api/ticket-service/ticket/search")
public Response<TrainQueryResponse> searchTrains(TrainQueryRequest searchCriteria) {
    return Response.success(ticketQueryService.fetchAvailableTrains(searchCriteria));
}

Vlaidating Request Parameters

Input validation is performed using a chain of responsibility pattern. The initial filter checks for mandatory fields.

@Component
public class MandatoryFieldValidator implements QueryValidationHandler<TrainQueryRequest> {

    @Override
    public void process(TrainQueryRequest criteria) {
        if (StringUtils.isEmpty(criteria.getOriginCode())) {
            throw new InvalidRequestException("Origin station code is required");
        }
        if (StringUtils.isEmpty(criteria.getDestinationCode())) {
            throw new InvalidRequestException("Destination station code is required");
        }
        if (criteria.getTravelDate() == null) {
            throw new InvalidRequestException("Travel date is required");
        }
    }

    @Override
    public int priority() {
        return 10;
    }
}

A subsequent validation step verifies the existence of station and region codes, utilizing a distributed cache to improve performance.

@Component
public class StationExistenceValidator implements QueryValidationHandler<TrainQueryRequest> {

    private final RegionRepository regionRepo;
    private final StationRepository stationRepo;
    private final CacheService cacheClient;
    private final DistributedLockFactory lockFactory;
    private static volatile boolean isCacheLoaded = false;

    @Override
    public void process(TrainQueryRequest criteria) {
        HashOperations<String, String, String> hashOps = cacheClient.getHashOperations();
        String cacheKey = "ALL_STATIONS_AND_REGIONS";
        List<String> codesToCheck = Arrays.asList(criteria.getOriginCode(), criteria.getDestinationCode());
        List<String> cachedValues = hashOps.multiGet(cacheKey, codesToCheck);
        long missingCount = cachedValues.stream().filter(Objects::isNull).count();
        if (missingCount == 0) {
            return;
        }
        if ((missingCount > 0 && isCacheLoaded && cacheClient.hasKey(cacheKey))) {
            throw new InvalidRequestException("Specified origin or destination does not exist");
        }
        DistributedLock lock = lockFactory.getLock("LOAD_STATION_DATA");
        lock.acquire();
        try {
            if (cacheClient.hasKey(cacheKey)) {
                cachedValues = hashOps.multiGet(cacheKey, codesToCheck);
                missingCount = cachedValues.stream().filter(Objects::nonNull).count();
                if (missingCount != 2) {
                    throw new InvalidRequestException("Specified origin or destination does not exist");
                }
                return;
            }
            List<Region> allRegions = regionRepo.findAll();
            List<Station> allStations = stationRepo.findAll();
            Map<String, String> cacheMap = new HashMap<>();
            allRegions.forEach(region -> cacheMap.put(region.getCode(), region.getName()));
            allStations.forEach(station -> cacheMap.put(station.getCode(), station.getRegionName()));
            hashOps.putAll(cacheKey, cacheMap);
            isCacheLoaded = true;
            long validCodeCount = cacheMap.keySet().stream()
                    .filter(code -> code.equals(criteria.getOriginCode()) || code.equals(criteria.getDestinationCode()))
                    .count();
            if (validCodeCount != 2) {
                throw new InvalidRequestException("Specified origin or destination does not exist");
            }
        } finally {
            lock.release();
        }
    }

    @Override
    public int priority() {
        return 20;
    }
}

Loading City and Region Data

To find all trains between two cities, the system first maps station codes to their parent city/region codes. This mapping is cached in a Redis hash.

List<String> regionNames = cacheClient.getHashOperations()
        .multiGet("STATION_REGION_MAP", Arrays.asList(request.getOriginCode(), request.getDestinationCode()));
long nullCount = regionNames.stream().filter(Objects::isNull).count();
if (nullCount > 0) {
    DistributedLock lock = lockFactory.getLock("POPULATE_REGION_MAP");
    lock.acquire();
    try {
        regionNames = cacheClient.getHashOperations()
                .multiGet("STATION_REGION_MAP", Arrays.asList(request.getOriginCode(), request.getDestinationCode()));
        nullCount = regionNames.stream().filter(Objects::isNull).count();
        if (nullCount > 0) {
            List<Station> stations = stationRepo.findAll();
            Map<String, String> stationRegionMapping = new HashMap<>();
            stations.forEach(station -> stationRegionMapping.put(station.getCode(), station.getRegionName()));
            cacheClient.getHashOperations().putAll("STATION_REGION_MAP", stationRegionMapping);
            regionNames = Arrays.asList(
                    stationRegionMapping.get(request.getOriginCode()),
                    stationRegionMapping.get(request.getDestinationCode())
            );
        }
    } finally {
        lock.release();
    }
}

Retrieving Train Route Information

Given the daily travel date constraint and the frontend handling most filtering, train schedules are cached in Redis rather than using a full-text search engine. The cached data structure is a hash keyed by the origin and destination region combintaion.

List<TrainScheduleDTO> scheduleResults = new ArrayList<>();
String regionPairKey = String.format("SCHEDULE:%s:%s", regionNames.get(0), regionNames.get(1));
Map<String, String> cachedSchedules = cacheClient.getHashOperations().entries(regionPairKey);
if (cachedSchedules.isEmpty()) {
    DistributedLock scheduleLock = lockFactory.getLock("LOAD_SCHEDULES_" + regionPairKey);
    scheduleLock.acquire();
    try {
        cachedSchedules = cacheClient.getHashOperations().entries(regionPairKey);
        if (cachedSchedules.isEmpty()) {
            List<Route> routes = routeRepository.findByRegions(regionNames.get(0), regionNames.get(1));
            for (Route route : routes) {
                Train trainInfo = cacheClient.getOrSet(
                        "TRAIN_INFO:" + route.getTrainId(),
                        () -> trainRepository.findById(route.getTrainId()).orElseThrow(),
                        360, TimeUnit.DAYS);
                TrainScheduleDTO schedule = new TrainScheduleDTO();
                schedule.setTrainId(route.getTrainId());
                schedule.setTrainNumber(trainInfo.getNumber());
                schedule.setDepartureTime(formatTime(route.getDeparture()));
                schedule.setArrivalTime(formatTime(route.getArrival()));
                schedule.setDuration(calculateDuration(route.getDeparture(), route.getArrival()));
                schedule.setOriginStation(route.getOriginStationName());
                schedule.setDestinationStation(route.getDestStationName());
                schedule.setOriginFlag(route.getOriginFlag());
                schedule.setDestFlag(route.getDestFlag());
                schedule.setTrainCategory(trainInfo.getCategory());
                schedule.setServiceBrand(trainInfo.getBrand());
                if (StringUtils.isNotEmpty(trainInfo.getTags())) {
                    schedule.setLabels(Arrays.asList(trainInfo.getTags().split(",")));
                }
                schedule.setDaysEnRoute(calculateDaysDifference(route.getDeparture(), route.getArrival()));
                schedule.setSaleStatus(new Date().after(trainInfo.getSaleStart()) ? "OPEN" : "PENDING");
                schedule.setSaleStartTime(formatDate(trainInfo.getSaleStart(), "MM-dd HH:mm"));
                scheduleResults.add(schedule);
                String cacheItemKey = route.getTrainId() + "_" + route.getOriginStationName() + "_" + route.getDestStationName();
                cachedSchedules.put(cacheItemKey, serialize(schedule));
            }
            if (!cachedSchedules.isEmpty()) {
                cacheClient.getHashOperations().putAll(regionPairKey, cachedSchedules);
            }
        }
    } finally {
        scheduleLock.release();
    }
}

After retrieving the basic schedules, they are sorted by departure time.

scheduleResults = scheduleResults.isEmpty()
        ? cachedSchedules.values().stream().map(this::deserializeSchedule).collect(Collectors.toList())
        : scheduleResults;
scheduleResults.sort(new DepartureTimeComparator());

Fetching Real-Time Seat Availability

Seat availability is dynamic and therefore fetched separately and attached to each schedule.

for (TrainScheduleDTO schedule : scheduleResults) {
    String priceCacheKey = String.format("PRICE:%s:%s:%s", schedule.getTrainId(), schedule.getOriginStation(), schedule.getDestinationStation());
    String priceDataJson = cacheClient.getOrSet(
            priceCacheKey,
            () -> {
                List<Fare> fares = fareRepository.findFares(schedule.getTrainId(), schedule.getOriginStation(), schedule.getDestinationStation());
                return serialize(fares);
            },
            360, TimeUnit.DAYS);
    List<Fare> fareList = deserializeFares(priceDataJson);
    List<SeatAvailability> availabilityList = new ArrayList<>();
    for (Fare fare : fareList) {
        String seatType = fare.getSeatClass();
        String remainingTicketsKey = "REMAINING:" + schedule.getTrainId() + "_" + fare.getOrigin() + "_" + fare.getDestination();
        Object availableSeatsObj = cacheClient.getHashOperations().get(remainingTicketsKey, seatType);
        int availableSeats = Optional.ofNullable(availableSeatsObj)
                .map(Object::toString)
                .map(Integer::parseInt)
                .orElseGet(() -> {
                    Map<String, Integer> seatMap = seatInventoryService.getInventory(schedule.getTrainId(), seatType, fare.getOrigin(), fare.getDestination());
                    return seatMap.getOrDefault(seatType, 0);
                });
        SeatAvailability seatInfo = new SeatAvailability(
                fare.getSeatClass(),
                availableSeats,
                fare.getPrice().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP),
                false // Selection flag
        );
        availabilityList.add(seatInfo);
    }
    schedule.setAvailableSeats(availabilityList);
}

Constructing the API Response

The final response aggregates the schedule list and builds distinct lists of filterable attributes (origin stations, destination stations, train brands, seat classes) derived from the result set.

return TrainQueryResponse.builder()
        .schedules(scheduleResults)
        .availableOrigins(extractOrigins(scheduleResults))
        .availableDestinations(extractDestinations(scheduleResults))
        .availableBrands(extractBrands(scheduleResults))
        .availableSeatClasses(extractSeatClasses(scheduleResults))
        .build();

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.