Skip to content

Services

Services hold the business logic between controllers and repositories. Two patterns from the ESPN project I kept: re-fetching related entities before cascading saves, and applying partial PATCH updates field by field.

Pattern

@Service
public class PlayerService {

    private final PlayerRepository playerRepository;
    private final AwardRepository awardRepository;
    private final TeamRepository teamRepository;

    @Autowired
    public PlayerService(PlayerRepository playerRepository,
                         AwardRepository awardRepository,
                         TeamRepository teamRepository) {
        this.playerRepository = playerRepository;
        this.awardRepository = awardRepository;
        this.teamRepository = teamRepository;
    }

    public List<Player> findAll() {
        return playerRepository.findAll();
    }

    public Optional<Player> findById(Long id) {
        return playerRepository.findById(id);
    }

    public Player save(Player player) {
        // Cascading new received awards: re-fetch the award from the DB
        if (player.getReceivedAwards() != null) {
            player.getReceivedAwards().forEach(awardReceived -> {
                Optional<Award> award = awardRepository.findById(awardReceived.getAward().getId());
                award.ifPresent(awardReceived::setAward);
                awardReceived.setPlayer(player);
            });
        }

        // Cascading teams played for: replace detached entities with managed ones
        if (player.getTeamsPlayedFor() != null) {
            for (int i = 0; i < player.getTeamsPlayedFor().size(); i++) {
                Team team = player.getTeamsPlayedFor().get(i);
                Optional<Team> teamFromDb = teamRepository.findById(team.getId());
                int finalI = i;
                teamFromDb.ifPresent(fetchedTeam -> player.getTeamsPlayedFor().set(finalI, fetchedTeam));
            }
        }

        return playerRepository.save(player);
    }

    public Player patch(Player existingPlayer, Map<String, Object> updates) {
        for (Map.Entry<String, Object> entry : updates.entrySet()) {
            switch (entry.getKey()) {
                case "firstName": existingPlayer.setFirstName((String) entry.getValue()); break;
                case "lastName":  existingPlayer.setLastName((String) entry.getValue()); break;
                case "position":  existingPlayer.setPosition((Position) entry.getValue()); break;
                case "team":      existingPlayer.setTeam((Team) entry.getValue()); break;
                case "franchiseTagged": existingPlayer.setFranchiseTagged((Boolean) entry.getValue()); break;
            }
        }
        return playerRepository.save(existingPlayer);
    }

    public void deleteById(Long id) {
        playerRepository.deleteById(id);
    }
}

Why the re-fetch matters

Incoming JSON carries detached references (an award id, a team id). JPA cascades save whatever object graph it's handed — so resolve each reference to a managed entity from its repository first, then save. Otherwise you risk inserting duplicates or stale rows.

The patch method is the service-side half of the PATCH endpoint in Controllers — the controller parses the map, the service applies it.

See also