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

asked Jul 25, 2023 at 13:15
\$\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.

answered Jul 30, 2023 at 17:14
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.