TigerCard - Fare Calculation Problem

With the recent introduction on Java 17 by Oracle,  I thought to revise the software  principles (like SOLID, KISS, YAGNI etc) which are the building blocks of any software development & the best way to do that is by solving a programming problem. 

This solution does not use any framework like Spring boot, DB's etc. so that the solution remains simple. So without wasting any more time lets dive right into the problem. 

Problem Statement: 

The problem statement is to design the fare calculation engine called as TigerCard. The transport officials from the city of Atlanta want to design a payment system for public metro transport. They have come up with the idea of a prepaid card — TigerCard — which is an NFC enabled card that is to be tapped at entry and exit points of the metro stations.


Fare Calculation Engine

Detailed excerpt of the problem statement is below :

Explanation:

I have solved the problem by applying various design patterns like Strategy, Chain of Responsibility, Builder etc. I have tried my best to apply principle like SOLID, YAGNI, KISS etc. 

The Code is structured according to the SOLID principles into packages, classes etc. The starting point is Application.java whose job is to set/inject the dependencies before the start of the application and then call the regular Flow via the FareProcessor.java which calls the CalculationService.java (injected with various Calculation strategies earlier in FareProcessor.java)

To keep things simple as iterated earlier, frameworks are avoided even for validations, annotation processor implementations are written For e.g.  Date Annotation 


  
  @Documented
@Retention(RUNTIME)
@Target({ FIELD, LOCAL_VARIABLE })
@Constraint(validatedBy = DateConstraintValidator.class)
public @interface Date
{
	String message() default "Must be of the format dd-MM-yyyy";

	Class[] groups() default {};

	Class[] payload() default {};
}

  
And Below is its Validator:

  public class DateConstraintValidator implements ConstraintValidator
{
	@Override
	public boolean isValid(String validDate, ConstraintValidatorContext context)
	{
		context.disableDefaultConstraintViolation();
		var valid = false;
		try
		{
			// dd-MM-yyyy
			String[] splitDate = validDate.split("-");
			valid = LocalDate.of(Integer.valueOf(splitDate[2]), Integer.valueOf(splitDate[1]), Integer.valueOf(splitDate[0])) != null;
		}
		catch (Exception e)
		{}
		return valid;
	}
}
  

So similar way to validate the input I have created Time & Zone Annotations  its corresponding validators.

Usage of Chain of Responsibility: 

The interface FareStrategy.java is the one which abstracts away all the responsibility of the calculation rules into its own functional units. 

Calculation Rules are implemented via the Chain of Responsibility pattern so that the next chain calculates & applies the next rules and are independent with each other. This approach makes them much more maintainable and extendable as when needed. Below are the list of 3 strategies implemented:

  • DailyCappedFareStrategy
  • TimeBasedFareStrategy
  • WeeklyCappedFareStrategy

TimeBasedFareStrategy takes care of the calculating the fare based on the time and weekday constraint's given in the problem, DailyCappedFareStrategy takes care of capping of the fare on the daily basis while the WeeklyCappedFareStrategy does the same thing on week basis.

Link to my github solution is below:


What am I missing here ? Let me know in comments section and I'll add in!
What’s next? Subscribe Learn INQuiZitively to be the first to read my stories.