Friday, April 6, 2018

Delhi Metro Design Problem: Spring Hibernate and Core Java

Coding Exercise for Designing Smart Card System for Delhi Metro.

1. Requirement Scope

  1. Develop an API to calculate total footfall on a given station ( swipe in + swipe out)
  2. API to generate per card report on demand i.e. print all journey details for a given smart card - source station, destination station, date & time of travel, balance, fare, etc.

Detailed problem statement

Coding Exercise for Designing Smart Card System for Delhi Metro. implement ‘Metro Smart Card System’ (MSCS) for Delhi city. For application assume there is a single metro line covering 10 stations linearly. The stations name are A1, A2, A3, A4, A5, A6, A7, A8, A9, A10 as shown below. The travel can be in any direction.
Travelers have smart cards that behave just like any regular debit card that has an initial balance when purchased. Travelers swipe-in when they enter a metro station and swipe-out when they exit. The card balance is automatically updated at swipe-out.
Objective of the exercise is to create an automated system that has following functionalities:
  1. Card should have a minimum balance of Rs 5.5 at swipe-in. At swipe-out, system should calculate the fare based on below strategies set at the start of the day. The fare must be deducted from the card.
  2. Card should have the sufficient balance otherwise user should NOT be able to exit. Weekday – Rs. 7 * (Number of stations traveled) Weekend – Rs. 5.5 * (Number of station traveled if it’s Saturday or Sunday) (* there can be more such fare strategies in future)

Coding Exercise Evaluation Criteria

  •  Code Completeness/ Correctness
  •  Code Structure and quality: Modularity, usage of OO principles, * [x] size of classes/functions,
  •  Choice of data structures
  •  Unit Test cases
  •  Coding productivity (more time you take to submit the exercise, lesser you will score)
  •  class/function/variable names, package/class structure

2. Design is not meant for Production Use

The motive of this exercise is to evaluate candidate skills rather than developing a production ready software for Delhi Metro. In a real production environment, the technology stack may differ significantly. In real life scenario, for example,
  1. lot of hardware interaction will be required for smart card sensor devices connectivity with servers.
  2. caching needs to be implemented for quick swipe in/ swipe out logic.
  3. there will be a RDBMS for storing all the transactional data (Traveler Information, Smart Card, Journey Details, Station information, etc), or there may be NoSQL database as well.
  4. we will probably use a framework like spring/hibernate for seamless development and thus not rewriting the boilerplate code again and again.
  5. real production application may be hosted on a cloud to meet changing infrastructural requirements of the software, for example morning and evening office hours could need much more processing power than off hours. Thus some kind of Elastic Computing solution will be required in real scenario.
Considering it as a purely skills evaluation exercise, Let’s move on to the solution now.

3. Identifying the Domain Model

There are for identifiable domain models in this design, namely:
  1. Traveler or the User
  2. Smart Card
  3. Station
  4. Journey Details (CardTrx)
domain
Domain Models

4. Starting with TestCases

Lets start with all the Testcases that cover our scope of requirements for designing this application.
MetroServiceTest - defining the specs first
import org.hamcrest.CustomMatcher;
import org.junit.Before;
import org.junit.Test;

import java.time.LocalDateTime;
import java.time.Month;
import java.util.List;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItemInArray;
import static org.junit.Assert.assertThat;

public class MetroServiceTest {
    private MetroService metroService = new MetroService();

    private SmartCard card;

    @Before
    public void setUp() throws Exception {
        card = new SmartCard();
        card.setId(1);
        card.setBalance(100);
        card.setTraveller(new Traveller(1L, "Munish"));
    }

    @Test
    public void testCalculateFootFallForStation() throws Exception {
        metroService.swipeIn(card, Station.A1, LocalDateTime.of(2016, Month.APRIL, 8, 18, 25));
        metroService.swipeOut(card, Station.A6, LocalDateTime.of(2016, Month.APRIL, 8, 18, 35));

        metroService.swipeIn(card, Station.A6, LocalDateTime.of(2016, Month.APRIL, 10, 19, 05));
        metroService.swipeOut(card, Station.A10, LocalDateTime.of(2016, Month.APRIL, 10, 19, 15));

        assertThat("FootFall for station A6 should be 2", metroService.calculateFootFall(Station.A6), equalTo(2));
        assertThat("FootFall for station A1 should be 1", metroService.calculateFootFall(Station.A1), equalTo(1));
        assertThat("FootFall for station A10 should be 1", metroService.calculateFootFall(Station.A10), equalTo(1));
    }

    @Test
    public void testCardReport() throws Exception {
        metroService.swipeIn(card, Station.A1, LocalDateTime.of(2016, Month.APRIL, 8, 18, 25));
        metroService.swipeOut(card, Station.A6, LocalDateTime.of(2016, Month.APRIL, 8, 18, 35));

        metroService.swipeIn(card, Station.A6, LocalDateTime.of(2016, Month.APRIL, 10, 19, 05));
        metroService.swipeOut(card, Station.A10, LocalDateTime.of(2016, Month.APRIL, 10, 19, 15));
        final List<CardTrx> trxs = metroService.cardReport(card);

        assertThat("There should be 2 trxs for this card", trxs.size(), equalTo(2));
        assertThat("One of the Trx should be charged 35", trxs.toArray(new CardTrx[0]), hasItemInArray(new CustomMatcher<CardTrx>("Fare shall be 35") {
            @Override
            public boolean matches(Object o) {
                CardTrx trx = (CardTrx) o;
                return trx.getFare() == 35.0 && trx.getFareStrategyUsed() instanceof WeekdayFareStrategy && trx.distance == 5;
            }
        }));

        assertThat("Other Trx should be charged 35", trxs.toArray(new CardTrx[0]), hasItemInArray(new CustomMatcher<CardTrx>("Fare shall be 35") {
            @Override
            public boolean matches(Object o) {
                CardTrx trx = (CardTrx) o;
                return trx.getFare() == 22.0 && trx.getFareStrategyUsed() instanceof WeekendFareStrategy && trx.distance == 4;
            }
        }));
    }

    @Test(expected = MinimumCardBalanceException.class)
    public void testMinimumBalanceAtSwipeIn() throws Exception {
        card.setBalance(1);
        metroService.swipeIn(card, Station.A1, LocalDateTime.of(2016, Month.APRIL, 8, 18, 25));
    }

    @Test(expected = InsufficientCardBalance.class)
    public void testSufficientBalanceAtSwipeOut() throws Exception {
        card.setBalance(10);
        metroService.swipeIn(card, Station.A1, LocalDateTime.of(2016, Month.APRIL, 8, 18, 25));
        metroService.swipeOut(card, Station.A6, LocalDateTime.of(2016, Month.APRIL, 8, 18, 35));
    }
}

5. Handling different FareStrategy for Weekday and Weekend

As a part of requirement, we need to implement different fare strategy for weekdays and weekends. Fare would be less on weekends compared to weekdays since traffic is lesser on weekends.
strategy pattern
Fare Strategy - Two different implementations
FareStrategy Interface FareStrategy interface will have getFarePerStation method that will return fare rate for different implementations.
public interface FareStrategy {
    String getName();

    double getFarePerStation();
}
Now we will provide FareStrategy implementation for Weekdays in below class.
WeekdayFareStrategy
public class WeekdayFareStrategy implements FareStrategy {
    @Override
    public String getName() {
        return WeekdayFareStrategy.class.toGenericString();
    }

    @Override
    public double getFarePerStation() {
        return 7;
    }
}
Weekend Fare would be different, so we will create a new implementation for FareStrategy for the weekends.
WeekendFareStrategy
public class WeekendFareStrategy implements FareStrategy {

    @Override
    public String getName() {
        return WeekendFareStrategy.class.toGenericString();
    }

    @Override
    public double getFarePerStation() {
        return 5.5;
    }
}
We will create a Factory Class for creating appropriate instance of FareStrategy based on date-time
FareStrategyFactory Class (Factory Design Pattern) FareStrategyFactory will create appropriate instance of FareStrategy implementation i.e. one of WeekendFareStrategy and WeekdayFareStrategy.
FareStrategyFactory.class
public class FareStrategyFactory {
    static final FareStrategy weekendStrategy = new WeekendFareStrategy();
    static final FareStrategy weekdayStrategy = new WeekdayFareStrategy();

    public static FareStrategy getFareStrategy(LocalDateTime localDateTime) {
        if (localDateTime.getDayOfWeek() == DayOfWeek.SATURDAY || localDateTime.getDayOfWeek() == DayOfWeek.SUNDAY) {
            return weekendStrategy;
        } else {
            return weekdayStrategy;
        }
    }
}

6. Exceptions for Metro Services

There are two exceptional scenarios given to us in this requirement.
  1. MinimumCardBalanceException - This exception will be raised if Traveller does not have minimum balance of Rs 5. at the time of Swipe In.
  2. InsufficientCardBalance - it will be raised if user does not have sufficient balance at the time of swipe out as per Journey charges.
Below is the UML class diagram for our Exception Hierarchy
metro exception hierarchy
Exception Hierarchy

7. Business Domain Objects

As we covered earlier, there are 4 Domain Objects (Traveller, SmartCard, Station, CardTrx) which will store domain related information in some data store. For brevity we will use InMemoryCardTrxRepository to store the data. In a production like environment, we might go for Hibernate with RDBMS or Spring with NoSql Data Store.
public class Traveller {
    long id;
    String name;

 // Getters and Setters not shown for brevity
}

public class SmartCard {
    long id;

    Traveller traveller;
    double balance;

 // Getters and Setters not shown for brevity
}

public enum Station {
    A1, A2, A3, A4, A5, A6, A7, A8, A9, A10;

    public int distance(Station other) {
        return Math.abs(other.ordinal() - this.ordinal());
    }
}


public class CardTrx {
    long id;

    SmartCard card;

    Station source;
    Station destination;

    int distance;

    LocalDateTime startTime;
    LocalDateTime endTime;

    double balance;
    double fare;

    FareStrategy fareStrategyUsed;

 // Getters and Setters not shown for brevity
}

8. DAO Layer and Services

public class InMemoryCardTrxRepository {

    private ConcurrentMap<SmartCard, CardTrx> transientTrxStore = new ConcurrentHashMap<>();
    private ConcurrentMap<SmartCard, List<CardTrx>> completedTrxStore = new ConcurrentHashMap<>();


    public void addCompletedTrx(SmartCard card, CardTrx trx){
        completedTrxStore.putIfAbsent(card, new ArrayList<>());
        completedTrxStore.get(card).add(trx);
    }

    public void addTransientTrx(SmartCard card, CardTrx trx){
        transientTrxStore.put(card, trx);
    }

    public CardTrx getTransientTrx(SmartCard card) {
        return transientTrxStore.remove(card);
    }

    public List<CardTrx> getCompletedTrxs(SmartCard card) {
        return completedTrxStore.getOrDefault(card, Collections.emptyList());
    }
}
public class MetroService {
    private ConcurrentMap<Station, AtomicInteger> stationFootFall = new ConcurrentHashMap<>();

    private InMemoryCardTrxRepository trxRepository = new InMemoryCardTrxRepository();
    private FareCalculator fareCalculator = new FareCalculator();

    public void swipeIn(SmartCard card, Station source, LocalDateTime dateTime) {
        if (card.getBalance() < 5.5) {
            throw new MinimumCardBalanceException("Minimum balance of Rs 5.5 is required at Swipe In");
        }
        stationFootFall.putIfAbsent(source, new AtomicInteger());
        stationFootFall.get(source).incrementAndGet();
        CardTrx trx = new CardTrx();
        trx.setSource(source);
        trx.setCard(card);
        trx.setStartTime(dateTime);
        trxRepository.addTransientTrx(card, trx);
    }

    public void swipeOut(SmartCard card, Station destination, LocalDateTime dateTime) {
        stationFootFall.putIfAbsent(destination, new AtomicInteger());
        stationFootFall.get(destination).incrementAndGet();
        CardTrx trx = trxRepository.getTransientTrx(card);
        trx.setDestination(destination);
        trx.setEndTime(dateTime);
        trx.setDistance(destination.distance(trx.source));
        trx.setFare(fareCalculator.getFare(trx.source, trx.destination, dateTime));
        if (trx.getFare() > card.getBalance()) {
            throw new InsufficientCardBalance("Insufficient balance at Swipe Out, Please recharge card and try again");
        }
        trx.setFareStrategyUsed(FareStrategyFactory.getFareStrategy(dateTime));
        trx.setBalance(card.getBalance() - trx.getFare());
        card.setBalance(card.getBalance() - trx.getFare());
        trxRepository.addCompletedTrx(card, trx);
    }


    public int calculateFootFall(Station station) {
        return stationFootFall.getOrDefault(station, new AtomicInteger(0)).get();
    }

    public List<CardTrx> cardReport(SmartCard card) {
        List<CardTrx> trxs = trxRepository.getCompletedTrxs(card);
        trxs.forEach(trx -> {
            System.out.println("trx = " + trx);
        });
        return trxs;
    }
}
FareCalculator.java
public class FareCalculator {

    public double getFare(Station source, Station destination, LocalDateTime localDateTime) {
        FareStrategy fareStrategy = FareStrategyFactory.getFareStrategy(localDateTime);
        int distance = source.distance(destination);

        double fare = distance * fareStrategy.getFarePerStation();

        return fare;
    }

}

9. TODO

More details will be added later on!

10. Download PDF and Source Code

No comments:

Post a Comment

Your comment will be published after review from moderator