1
\$\begingroup\$

In a recruitment interview I got this small projet to do at home: rest API written in Java using Spring Boot. As I didn't hear back from the company I wanted to have feedback from review. I am a rest api/spring boot beginner.

Requirements: Design a rest API managing horse races and their particpants:

  • A race hase a name, date and unique number for that date
  • A race has at least 3 participants
  • A particpant has a name and an unique id
  • The participant in a race have consecutive ids starting with 1

The API must expose methods for:

  • Create races
  • Create their participants
  • Persist both in a sql db
  • Add Kafka producer/consumer layer so that the races could be read from systems outside ( As I've never used Kafka, I didn't do that part)

Here is my code, following Controller -> Service -> Repository pattern:

  • Domain

/***RACE***/

public interface IRace {

    int MIN_COURSE_SIZE = 3;

    String getName();
    LocalDate getDate();
    int getDayNumber();
    Set<IRunner> getRunners();
    void addRunner(String name);

}


@Entity
@Table(name = "RACE")
@RequiredArgsConstructor
@AllArgsConstructor
public class Race implements IRace {

    private AtomicInteger runnersCount = new AtomicInteger(1);

    @EmbeddedId
    @Getter
    RaceId id;

    @OneToMany(mappedBy = "race", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @Getter
    private Set<IRunner> runners = new HashSet<>();

    public Race(RaceId id) {
        this.id = id;
    }

    @Override
    public String getName() {
        return id.getName();
    }

    @Override
    public LocalDate getDate() {
        return id.getDate();
    }

    @Override
    public int getDayNumber() {
        return id.getDayNumber();
    }

    @Override
    public Set<IRunner> getRunners() {
        return runners;
    }

    @Override
    public void addRunner(String name) {
        runners.add(new Runner(name, runnersCount.getAndIncrement(), this));
    }

    @Override
    public int hashCode() {
        return Objects.hash(id.getName(), id.getDate(), id.getDayNumber());
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Race))
            return false;
        Race other = (Race) o;
        return this.id.equals(other.id);
    }

    @Override
    public String toString() {
        return "[Race: name=" + getName() + " date=" + getDate() + " dayNumber=" + getDayNumber() + " runners=" + runnersCount;
    }
}



@Embeddable
@NoArgsConstructor
@AllArgsConstructor
public class RaceId implements Serializable {

    private static final long serialVersionUID = -9092062626878526790L;

    @Column(name = "race_name", nullable = false)
    @Getter
    private String name;

    @Column(name = "race_date", nullable = false)
    @Getter
    private LocalDate date;

    @Column(name="race_day_number",nullable = false)
    @Positive
    @Getter
    private int dayNumber;

    @Override
    public int hashCode() {
        return Objects.hash(name, date, dayNumber);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof RaceId))
            return false;
        RaceId other = (RaceId) obj;
        return this.name.equals(other.name) && this.date.equals(other.date) && this.dayNumber == other.dayNumber;
    }

    public String getName() {
        return name;
    }

    public LocalDate getDate() {
        return date;
    }

    public int getDayNumber() {
        return dayNumber;
    }
}


public class RaceValidator implements ConstraintValidator<ValidRace, IRace> {
    @Override
    public void initialize(ValidRace constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(IRace race, ConstraintValidatorContext constraintValidatorContext) {
        if (!isValid(race)) {
            throw new IllegalArgumentException("Invalid race");
        }
        return true;
    }

    /**
     * Validates following rules:
     * 1. Course runners must be at least n (n = ICourse.MinCourseSize)
     * 2. Runner numbers must be n first integers starting from 1
     * 3. Runners must all be part of the same race
     *
     * @param race
     * @return true if race satisfies the conditions, false otherwise
     */
    private boolean isValid(IRace race) {
        int n = race.getRunners().size();
        if (n <= IRace.MIN_COURSE_SIZE) {
            return false;
        }
        int validSum = (n * (n + 1)) / 2;
        int sum = race.getRunners().stream().mapToInt(r -> r.getNumber()).sum();

        if (sum != validSum) {
            return false;
        }

        if (!race.getRunners().stream().allMatch(r -> r.getCourse().equals(race))) {
            return false;
        }
        return true;
    }
}

@Constraint(validatedBy = RaceValidator.class)
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidRace {
    String message() default "Invalid course";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}


/*** RUNNER ***/

public interface IRunner {

    String getName();

    int getNumber();
    void setNumber(int number);

    IRace getCourse();

}


@Entity
@Table(name = "RUNNER")
@NoArgsConstructor
public class Runner implements IRunner {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Getter
    private Long id;

    @Column(name = "NAME", nullable = false)
    @Getter
    private String name;

    @Column(name = "NUMBER")
    @Getter
    @Setter
    private int number;

    @ManyToOne
    @JoinColumns({
            @JoinColumn(
                    name = "race_name",
                    referencedColumnName = "race_name"),
            @JoinColumn(
                    name = "race_date",
                    referencedColumnName = "race_date"),
            @JoinColumn(
                    name = "race_day_number",
                    referencedColumnName = "race_day_number")
    })
    @Getter
    private IRace race;

    public Runner(String name, int number, IRace course) {
        this.name = name;
        this.number = number;
        this.race = course;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, number, race);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Runner))
            return false;
        Runner other = (Runner) o;
        return this.name.equals(other.name) && this.number == other.number && this.race.equals(other.race);
    }
}

  • Repository
public interface RaceRepository extends CrudRepository<IRace, RaceId> {

    Iterable<IRace> findByDate(LocalDate date);
    Optional<IRace> findByDateAndDayNumber(LocalDate date, int dayNumber);
}

  • Service

public interface IRaceService {

    IRace save(IRace course);

    Iterable<IRace> findAll();

    Iterable<IRace> findByDate(LocalDate date);

    Optional<IRace> findByDateAndDayNumber(LocalDate date, int dayNumber);
}

@Slf4j
@Service
public class RaceService implements IRaceService {

    @Autowired
    private RaceRepository raceRepository;

    @Override
    public IRace save(IRace race) {
        return raceRepository.save(race);
    }

    @Override
    public Iterable<IRace> findAll() {
        return raceRepository.findAll();
    }

    @Override
    public Iterable<IRace> findByDate(org.joda.time.LocalDate date) {
        return raceRepository.findByDate(date);
    }

    @Override
    public Optional<IRace> findByDateAndDayNumber(LocalDate date, int dayNumber) {
        return raceRepository.findByDateAndDayNumber(date, dayNumber);
    }
}

public class InvalidRaceException extends Exception {
    public InvalidRaceException(String s) {
        super(s);
    }
}

  • Controller
/**
 * Generic wrapper for http response
 */

@Data
@SuperBuilder
@JsonInclude(Include.NON_NULL)
public class GenericResponse {

    protected LocalDateTime timeStamp;
    protected HttpStatus httpStatus;
    protected String reason;
    protected String message;
    protected String developerMessage;
    protected Map data;

}


import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;

@Validated
@Controller
@RequestMapping("/race")
public class RaceController {

    @Autowired
    private IRaceService raceService;

    @GetMapping("/races")
    public ResponseEntity<GenericResponse> findAll() {
        GenericResponse genericResponse = GenericResponse.builder().build();
        genericResponse.timeStamp = LocalDateTime.now();
        genericResponse.data = Map.of("races", raceService.findAll());
        genericResponse.message = "races found";
        genericResponse.httpStatus = OK;
        return ResponseEntity.ok(genericResponse);
    }

    @GetMapping(value = "/race/{date}")
    public ResponseEntity<GenericResponse> findByDate(@PathVariable("date") LocalDate date) {
        GenericResponse genericResponse = GenericResponse.builder().build();
        genericResponse.timeStamp = LocalDateTime.now();
        genericResponse.data = Map.of("races", raceService.findByDate(date));
        genericResponse.message = "races found for date: " + date;
        genericResponse.httpStatus = OK;
        return ResponseEntity.ok(genericResponse);
    }

    @GetMapping(value = "/race", params = {"date", "dayNumber"})
    public ResponseEntity<GenericResponse> findByDateAndDayNumber(@PathVariable("date") LocalDate date, @PathVariable("dayNumber") int dayNumber) {
        GenericResponse genericResponse = GenericResponse.builder().build();
        genericResponse.timeStamp = LocalDateTime.now();
        genericResponse.data = Map.of("races", raceService.findByDateAndDayNumber(date, dayNumber));
        genericResponse.message = "races found";
        genericResponse.httpStatus = OK;
        return ResponseEntity.ok(genericResponse);
    }

    @PostMapping("/race")
    public ResponseEntity<GenericResponse> addRace(@RequestBody @ValidRace IRace race) {
        GenericResponse genericResponse = GenericResponse.builder().build();
        genericResponse.timeStamp = LocalDateTime.now();
        genericResponse.data = Map.of("races", raceService.save(race));
        genericResponse.message = "race created";
        genericResponse.httpStatus = CREATED;
        return ResponseEntity.ok(genericResponse);
    }

    @PostMapping("/race/{runnerName}")
    public ResponseEntity<GenericResponse> addRunner(@RequestBody @ValidRace IRace race, @PathVariable String runnerName) {
        race.addRunner(runnerName);
        GenericResponse genericResponse = GenericResponse.builder().build();
        genericResponse.timeStamp = LocalDateTime.now();
        genericResponse.data = Map.of("races", raceService.save(race));
        genericResponse.message = "race created";
        genericResponse.httpStatus = CREATED;
        return ResponseEntity.ok(genericResponse);
    }

}

Any critics are welcomed. Thank you very much

\$\endgroup\$
1

1 Answer 1

1
\$\begingroup\$
@GetMapping("/races")
public ResponseEntity<GenericResponse> findAll() {
    GenericResponse genericResponse = GenericResponse.builder().build();
    genericResponse.timeStamp = LocalDateTime.now();
    genericResponse.data = Map.of("races", raceService.findAll());
    genericResponse.message = "races found";
    genericResponse.httpStatus = OK;
    return ResponseEntity.ok(genericResponse);
}

findAll seems a problematic If you have a lot of data. you will encounter a lot of heap, and maybe HEAP OVERFLOW. what I will suggest is to use paging, or any other technique that prevent loading all the data in this way. there is a bunch of technique that you can use.

findByDate(org.joda.time.LocalDate date)

once you imported LocalDate, why to put the full path into method parameter ?

throw new IllegalArgumentException("Invalid race");

better to add the id of the race or any thing that indicate to the info for investigation logs.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.