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:
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++;
}
}
});
}
}
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());
}
}
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]
@Lazy
Annotation: I tried to use @Lazy
annotation on the dependencies to break the cycle, but the problem persists.I need a simpler solution that can resolve the cyclic dependency without significantly altering the structure of my application. How can I achieve this?
Here are my DBSeeder
and other related classes that might help provide more context:
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);
}
}
Thank you for your help!
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++;
}
}
});
}
}