springspring-boothibernatedependency-injectionlazy-initialization

Spring boot: dependency cycle between beans could not be broken


I am working on creating an Airline website using Spring Boot. I have a Seat class and a Flight class which represent tables in the database. The idea is that whenever a flight is added, seats for the flight are automatically created and added to the table.

Here are my service classes for adding flights and creating seats:

SeatService Implementation:

package com.example.airline.flight.seat;

import com.example.airline.flight.FlightService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class SeatServiceImp implements SeatService {
    private final SeatRepository seatRepository;
    private final @Lazy FlightService flightService; // Used @Lazy to break the cycle

    @Override
    public void createSeat(long flightId) {
        flightService.getSeatNumbers(flightId).forEach((key, value) -> {
            int rowNumber = 1;
            char seatLetter = 'A';
            for (int i = 1; i <= value; i++) {
                String seatNumber = seatLetter + String.valueOf(rowNumber);
                Seat seat = Seat.builder()
                        .seatNumber(seatNumber)
                        .seatClass(SeatClass.valueOf(key))
                        .flight(flightService.getFlightById(flightId))
                        .seatStatus(SeatStatus.AVAILABLE)
                        .build();
                seatRepository.save(seat);

                seatLetter++;
                if (seatLetter > 'F') {
                    seatLetter = 'A';
                    rowNumber++;
                }
            }
        });
    }
}

FlightService Implementation:

package com.example.airline.flight;

import com.example.airline.flight.seat.SeatService;
import com.example.airline.plane.Plane;
import com.example.airline.plane.PlaneService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
@RequiredArgsConstructor
public class FlightServiceImp implements FlightService {
    private final FlightRepository flightRepository;
    private final PlaneService planeService;
    private final @Lazy SeatService seatService; // Used @Lazy to break the cycle

    @Override
    public Map<String, Integer> getSeatNumbers(long flightId) {
        long planeId = flightRepository.findById(flightId).orElseThrow().getPlane().getId();
        return planeService.getSeatNumbers(planeId);
    }

    @Override
    public Flight getFlightById(long id) {
        return flightRepository.findById(id).orElseThrow();
    }

    @Override
    public void addFlight(Flight flight) {
        if (planeService.findPlaneById(flight.getPlane().getId()) == null) {
            throw new IllegalArgumentException("Plane not found");
        }
        Plane existingPlane = planeService.findPlaneById(flight.getPlane().getId());
        if (existingPlane == null || !existingPlane.getCurrentAirport().equals(flight.getDepartureAirport())) {
            throw new IllegalArgumentException("Plane not found");
        }
        flightRepository.save(flight);
        seatService.createSeat(flight.getId());
    }
}

The Problem:

When I run the application, I get a cyclic dependency error. Here’s the error message:

The dependencies of some of the beans in the application context form a cycle:

DBSeeder defined in file [C:\Users\Omar\Desktop\Airline_Backend\target\classes\com\example\airline\DBSeeder.class]
|  flightServiceImp defined in file [C:\Users\Omar\Desktop\Airline_Backend\target\classes\com\example\airline\flight\FlightServiceImp.class]
↑     ↓
|  seatServiceImp defined in file [C:\Users\Omar\Desktop\Airline_Backend\target\classes\com\example\airline\flight\seat\SeatServiceImp.class]

What I Have Tried:

  1. Using @Lazy Annotation: I tried to use @Lazy annotation on the dependencies to break the cycle, but the problem persists.
  2. Refactoring the Code: I considered an event-based approach to decouple the dependencies, but it would change the way my application works significantly.

The Goal:

I need a simpler solution that can resolve the cyclic dependency without significantly altering the structure of my application. How can I achieve this?

Additional Information:

Here are my DBSeeder and other related classes that might help provide more context:

DBSeeder Implementation:

package com.example.airline;

import com.example.airline.plane.aircraft.Aircraft;
import com.example.airline.plane.aircraft.AircraftService;
import com.example.airline.plane.Plane;
import com.example.airline.plane.PlaneService;
import com.example.airline.flight.Flight;
import com.example.airline.flight.FlightService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.sql.Date;
import java.sql.Time;
import java.time.LocalDate;

@Component
@RequiredArgsConstructor
public class DatabaseSeeder implements CommandLineRunner {

    private final AircraftService aircraftService;
    private final PlaneService planeService;
    private final @Lazy FlightService flightService; // Added @Lazy here to prevent early initialization

    @Override
    public void run(String... args) throws Exception {
        // Add Aircraft
        Aircraft aircraft1 = Aircraft.builder()
                .model("Boeing 737")
                .businessSeats(20)
                .businessSeatPrice(2000)
                .economySeats(150)
                .economySeatPrice(500)
                .firstClassSeats(10)
                .firstClassSeatPrice(5000)
                .rangeKm(6000)
                .build();

        aircraftService.addAircraft(aircraft1);

        Aircraft aircraft2 = Aircraft.builder()
                .model("Airbus A320")
                .businessSeats(15)
                .businessSeatPrice(1800)
                .economySeats(160)
                .economySeatPrice(600)
                .firstClassSeats(12)
                .firstClassSeatPrice(4500)
                .rangeKm(5800)
                .build();

        aircraftService.addAircraft(aircraft2);

        // Add Planes
        Plane plane1 = Plane.builder()
                .id(1L)
                .aircraft(aircraft1)
                .name("Plane 1")
                .lastMaintenanceDate(Date.valueOf(LocalDate.now()))
                .nextMaintenanceDate(Date.valueOf(LocalDate.now().plusDays(30))) // 30 days from now
                .hoursOnAir(1000)
                .flightsCompleted(200)
                .currentAirport("JFK")
                .operationlaStatus(OperationlaStatus.ACTIVE)
                .build();

        Plane plane2 = Plane.builder()
                .id(2L)
                .aircraft(aircraft1)
                .name("Plane 2")
                .lastMaintenanceDate(Date.valueOf(LocalDate.now()))
                .nextMaintenanceDate(Date.valueOf(LocalDate.now().plusDays(30))) // 30 days from now
                .hoursOnAir(1200)
                .flightsCompleted(220)
                .currentAirport("LAX")
                .operationlaStatus(OperationlaStatus.ACTIVE)
                .build();

        Plane plane3 = Plane.builder()
                .id(3L)
                .aircraft(aircraft2)
                .name("Plane 3")
                .lastMaintenanceDate(Date.valueOf(LocalDate.now()))
                .nextMaintenanceDate(Date.valueOf(LocalDate.now().plusDays(30))) // 30 days from now
                .hoursOnAir(800)
                .flightsCompleted(180)
                .currentAirport("JFK")
                .operationlaStatus(OperationlaStatus.ACTIVE)
                .build();

        planeService.addPlane(plane1);
        planeService.addPlane(plane2);
        planeService.addPlane(plane3);

        // Add Flights
        Flight flight1 = Flight.builder()
                .departureCity("New York")
                .arrivalCity("London")
                .departureTime(Time.valueOf("10:00:00"))
                .arrivalTime(Time.valueOf("20:00:00"))
                .departureDate(Date.valueOf(LocalDate.now()))
                .arrivalDate(Date.valueOf(LocalDate.now().plusDays(1))) // 1 day from now
                .departureAirport("JFK")
                .arrivalAirport("LHR")
                .departureTerminal("T4")
                .arrivalTerminal("T5")
                .departureCountry("USA")
                .arrivalCountry("UK")
                .plane(plane1)
                .build();

        Flight flight2 = Flight.builder()
                .departureCity("Los Angeles")
                .arrivalCity("Tokyo")
                .departureTime(Time.valueOf("14:00:00"))
                .arrivalTime(Time.valueOf("04:00:00"))
                .departureDate(Date.valueOf(LocalDate.now()))
                .arrivalDate(Date.valueOf(LocalDate.now().plusDays(1))) // 1 day from now
                .departureAirport("LAX")
                .arrivalAirport("HND")
                .departureTerminal("T2")
                .arrivalTerminal("T3")
                .departureCountry("USA")
                .arrivalCountry("Japan")
                .plane(plane2)
                .build();

        flightService.addFlight(flight1);
        flightService.addFlight(f

light2);
    }
}

What is the best way to resolve this cyclic dependency issue without significantly changing the structure of my application?

Thank you for your help!


Solution

  • When writing services or even service methods you should think usecases not entities. Your addFlight method should create the sets etc. and not delegate that to another service (the SeatService). Simply move that method to the FlightService make it private and call it from the method. I doubt you will ever need the createSeat method standalone.

    When you moved the method you can ditch the SeatService and simply inject the SeatRepository into the FlightService or even better, assuming Flight is a proper JPA entity and has relations to Seat you don't even need it and can just add the Seat to the Flight and save everything in one go.

    @Service
    @RequiredArgsConstructor
    public class FlightServiceImp implements FlightService {
    
        private final FlightRepository flightRepository;
        private final PlaneService planeService;
    
        @Override
        public Map<String, Integer> getSeatNumbers(long flightId) {
            long planeId = flightRepository.findById(flightId).orElseThrow().getPlane().getId();
            return planeService.getSeatNumbers(planeId);
        }
    
        @Override
        public Flight getFlightById(long id) {
            return flightRepository.findById(id).orElseThrow();
        }
    
        @Override
        public void addFlight(Flight flight) {
            Plane existingPlane = planeService.findPlaneById(flight.getPlane().getId());
            if (existingPlane == null) {
                throw new IllegalArgumentException("Plane not found");
            }
            if (existingPlane == null || !existingPlane.getCurrentAirport().equals(flight.getDepartureAirport())) {
                throw new IllegalArgumentException("Plane not found");
            }
            createSeats(flight);
            flightRepository.save(flight);
        }
    
        private void createSeats(Flight flight) {
          var planeId = flight.getPlane().getId();
          planeService.getSeatNumbers(planeId).forEach((key, value) -> {
            int rowNumber = 1;
            char seatLetter = 'A';
            for (int i = 1; i <= value; i++) {
              String seatNumber = seatLetter + String.valueOf(rowNumber);
              Seat seat = Seat.builder()
                          .seatNumber(seatNumber)
                          .seatClass(SeatClass.valueOf(key))
                          .flight(flight
                          .seatStatus(SeatStatus.AVAILABLE)
                          .build();
               flight.addSeat(seat);
               seatLetter++;
               if (seatLetter > 'F') {
                 seatLetter = 'A';
                 rowNumber++;
               }
             }
           });
        }
    }