This document outlines the implementation of a full-stack food delivery application using Spring Boot for the backend and a RESTful API design. The application includes user management, restaurant management, and customer-facing features, with DTOs for data transfer, centralized exception handling, and ModelMapper for entity-DTO conversions.

1. Project Setup

2. Entities and DTOs

Entities

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    private String firstName;

    @NotBlank
    private String lastName;

    @Email
    @NotBlank
    private String email;

    @NotBlank
    private String password;

    @Enumerated(EnumType.STRING)
    private UserRole role; // e.g., CUSTOMER, ADMIN

    @OneToOne(cascade = CascadeType.ALL)
    private Address address;
    // Getters and setters
}

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    private String street;

    @NotBlank
    private String city;

    @NotBlank
    private String zipCode;
    // Getters and setters
}

@Entity
public class Restaurant {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    private String name;

    @NotBlank
    private String location;

    @OneToMany(mappedBy = "restaurant", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<FoodItem> foodItems = new ArrayList<>();
    // Getters and setters
}

@Entity
public class FoodItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    private String name;

    @Min(0)
    private double price;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "restaurant_id")
    private Restaurant restaurant;
    // Getters and setters
}

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    private User user;

    @ManyToOne
    private Restaurant restaurant;

    @Enumerated(EnumType.STRING)
    private OrderStatus status; // e.g., PLACED, DELIVERED, CANCELLED

    private LocalDateTime orderTime;

    @OneToMany(cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();
    // Getters and setters
}

@Entity
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    private FoodItem foodItem;

    @Min(1)
    private int quantity;
    // Getters and setters
}

public enum UserRole {
    CUSTOMER, ADMIN
}

public enum OrderStatus {
    PLACED, DELIVERED, CANCELLED
}

DTOs

public class UserDTO {
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    private UserRole role;
    // Getters and setters
}

public class UserSignUpDTO {
    @NotBlank
    @Size(min = 4, max = 20)
    private String firstName;

    @NotBlank
    private String lastName;

    @Email
    @NotBlank
    private String email;

    @Pattern(regexp = "(?=.*\\\\d)(?=.*[a-z])(?=.*[#@$*]).{5,20}")
    private String password;

    private UserRole role;
    // Getters and setters
}

public class UserSignInDTO {
    @Email
    @NotBlank
    private String email;

    @NotBlank
    private String password;
    // Getters and setters
}

public class AddressDTO {
    private String street;
    private String city;
    private String zipCode;
    // Getters and setters
}

public class RestaurantDTO {
    private Long id;
    private String name;
    private String location;

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private List<FoodItemDTO> foodItems;
    // Getters and setters
}

public class FoodItemDTO {
    private Long id;
    private String name;
    private double price;
    // Getters and setters
}

public class OrderDTO {
    private Long id;
    private Long userId;
    private Long restaurantId;
    private OrderStatus status;
    private LocalDateTime orderTime;
    private List<OrderItemDTO> orderItems;
    // Getters and setters
}

public class OrderItemDTO {
    private Long foodItemId;
    private int quantity;
    // Getters and setters
}

public class ApiResponse {
    private String message;
    private LocalDateTime timestamp;

    public ApiResponse(String message, LocalDateTime timestamp) {
        this.message = message;
        this.timestamp = timestamp;
    }
    // Getters and setters
}

3. ModelMapper Configuration

@Configuration
public class AppConfig {
    @Bean
    public ModelMapper modelMapper() {
        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration()
            .setMatchingStrategy(MatchingStrategies.STRICT)
            .setPropertyCondition(Conditions.isNotNull());
        return mapper;
    }
}

4. Repositories

public interface UserDao extends JpaRepository<User, Long> {
    Optional<User> findByEmailAndPassword(String email, String password);
}

public interface RestaurantDao extends JpaRepository<Restaurant, Long> {}

public interface FoodItemDao extends JpaRepository<FoodItem, Long> {}

public interface OrderDao extends JpaRepository<Order, Long> {
    List<Order> findByStatus(OrderStatus status);
}

5. Service Layer

@Service
@AllArgsConstructor
public class UserService {
    private final UserDao userDao;
    private final ModelMapper modelMapper;

    public UserDTO signUp(UserSignUpDTO signUpDTO) {
        if (userDao.findByEmailAndPassword(signUpDTO.getEmail(), signUpDTO.getPassword()).isPresent()) {
            throw new IllegalArgumentException("User already exists");
        }
        User user = modelMapper.map(signUpDTO, User.class);
        User savedUser = userDao.save(user);
        return modelMapper.map(savedUser, UserDTO.class);
    }

    public UserDTO signIn(String email, String password) {
        User user = userDao.findByEmailAndPassword(email, password)
            .orElseThrow(() -> new AuthenticationException("Invalid credentials"));
        return modelMapper.map(user, UserDTO.class);
    }

    public UserDTO assignAddress(Long userId, Address address) {
        User user = userDao.findById(userId)
            .orElseThrow(() -> new ResourceNotFoundException("User not found"));
        user.setAddress(address);
        User updatedUser = userDao.save(user);
        return modelMapper.map(updatedUser, UserDTO.class);
    }
}

@Service
@AllArgsConstructor
public class RestaurantService {
    private final RestaurantDao restaurantDao;
    private final FoodItemDao foodItemDao;
    private final ModelMapper modelMapper;

    public List<RestaurantDTO> getAllRestaurants() {
        return restaurantDao.findAll().stream()
            .map(restaurant -> modelMapper.map(restaurant, RestaurantDTO.class))
            .collect(Collectors.toList());
    }

    public RestaurantDTO getRestaurantById(Long id) {
        Restaurant restaurant = restaurantDao.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Restaurant not found"));
        return modelMapper.map(restaurant, RestaurantDTO.class);
    }

    public RestaurantDTO addRestaurant(RestaurantDTO restaurantDTO) {
        Restaurant restaurant = modelMapper.map(restaurantDTO, Restaurant.class);
        Restaurant savedRestaurant = restaurantDao.save(restaurant);
        return modelMapper.map(savedRestaurant, RestaurantDTO.class);
    }

    public RestaurantDTO updateRestaurant(Long id, RestaurantDTO restaurantDTO) {
        Restaurant restaurant = restaurantDao.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Restaurant not found"));
        restaurant.setName(restaurantDTO.getName());
        restaurant.setLocation(restaurantDTO.getLocation());
        Restaurant updatedRestaurant = restaurantDao.save(restaurant);
        return modelMapper.map(updatedRestaurant, RestaurantDTO.class);
    }

    public void deleteRestaurant(Long id) {
        Restaurant restaurant = restaurantDao.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Restaurant not found"));
        restaurantDao.delete(restaurant);
    }

    public FoodItemDTO addFoodItem(Long restaurantId, FoodItemDTO foodItemDTO) {
        Restaurant restaurant = restaurantDao.findById(restaurantId)
            .orElseThrow(() -> new ResourceNotFoundException("Restaurant not found"));
        FoodItem foodItem = modelMapper.map(foodItemDTO, FoodItem.class);
        foodItem.setRestaurant(restaurant);
        FoodItem savedFoodItem = foodItemDao.save(foodItem);
        return modelMapper.map(savedFoodItem, FoodItemDTO.class);
    }

    public void deleteFoodItem(Long restaurantId, Long foodItemId) {
        Restaurant restaurant = restaurantDao.findById(restaurantId)
            .orElseThrow(() -> new ResourceNotFoundException("Restaurant not found"));
        FoodItem foodItem = foodItemDao.findById(foodItemId)
            .orElseThrow(() -> new ResourceNotFoundException("Food item not found"));
        if (!foodItem.getRestaurant().getId().equals(restaurantId)) {
            throw new IllegalArgumentException("Food item does not belong to the specified restaurant");
        }
        foodItemDao.delete(foodItem);
    }
}

@Service
@AllArgsConstructor
public class OrderService {
    private final OrderDao orderDao;
    private final UserDao userDao;
    private final RestaurantDao restaurantDao;
    private final FoodItemDao foodItemDao;
    private final ModelMapper modelMapper;

    public OrderDTO placeOrder(OrderDTO orderDTO) {
        User user = userDao.findById(orderDTO.getUserId())
            .orElseThrow(() -> new ResourceNotFoundException("User not found"));
        Restaurant restaurant = restaurantDao.findById(orderDTO.getRestaurantId())
            .orElseThrow(() -> new ResourceNotFoundException("Restaurant not found"));

        Order order = new Order();
        order.setUser(user);
        order.setRestaurant(restaurant);
        order.setStatus(OrderStatus.PLACED);
        order.setOrderTime(LocalDateTime.now());

        List<OrderItem> orderItems = orderDTO.getOrderItems().stream().map(itemDTO -> {
            FoodItem foodItem = foodItemDao.findById(itemDTO.getFoodItemId())
                .orElseThrow(() -> new ResourceNotFoundException("Food item not found"));
            OrderItem orderItem = new OrderItem();
            orderItem.setFoodItem(foodItem);
            orderItem.setQuantity(itemDTO.getQuantity());
            return orderItem;
        }).collect(Collectors.toList());

        order.setOrderItems(orderItems);
        Order savedOrder = orderDao.save(order);
        return modelMapper.map(savedOrder, OrderDTO.class);
    }

    public void cancelOrder(Long orderId) {
        Order order = orderDao.findById(orderId)
            .orElseThrow(() -> new ResourceNotFoundException("Order not found"));
        order.setStatus(OrderStatus.CANCELLED);
        orderDao.save(order);
    }

    public List<OrderDTO> getOrdersByStatus(OrderStatus status) {
        return orderDao.findByStatus(status).stream()
            .map(order -> modelMapper.map(order, OrderDTO.class))
            .collect(Collectors.toList());
    }

    public OrderDTO updateOrderStatus(Long orderId, OrderStatus status) {
        Order order = orderDao.findById(orderId)
            .orElseThrow(() -> new ResourceNotFoundException("Order not found"));
        order.setStatus(status);
        Order updatedOrder = orderDao.save(order);
        return modelMapper.map(updatedOrder, OrderDTO.class);
    }
}

6. REST Controller

@RestController
@RequestMapping("/api")
@AllArgsConstructor
public class FoodDeliveryController {
    private final UserService userService;
    private final RestaurantService restaurantService;
    private final OrderService orderService;

    // User Sign-Up
    @PostMapping("/users/signup")
    public ResponseEntity<UserDTO> signUp(@Valid @RequestBody UserSignUpDTO signUpDTO) {
        UserDTO user = userService.signUp(signUpDTO);
        return new ResponseEntity<>(user, HttpStatus.CREATED);
    }

    // User Sign-In
    @PostMapping("/users/signin")
    public ResponseEntity<?> signIn(@Valid @RequestBody UserSignInDTO signInDTO) {
        try {
            UserDTO user = userService.signIn(signInDTO.getEmail(), signInDTO.getPassword());
            return new ResponseEntity<>(user, HttpStatus.OK);
        } catch (AuthenticationException e) {
            return new ResponseEntity<>(new ApiResponse(e.getMessage(), LocalDateTime.now()), HttpStatus.UNAUTHORIZED);
        }
    }

    // Assign Address
    @PutMapping("/users/{userId}/address")
    public ResponseEntity<UserDTO> assignAddress(@PathVariable Long userId, @Valid @RequestBody AddressDTO addressDTO) {
        UserDTO user = userService.assignAddress(userId, modelMapper.map(addressDTO, Address.class));
        return new ResponseEntity<>(user, HttpStatus.OK);
    }

    // Admin: Add New Restaurant
    @PostMapping("/restaurants")
    public ResponseEntity<RestaurantDTO> addRestaurant(@Valid @RequestBody RestaurantDTO restaurantDTO) {
        RestaurantDTO savedRestaurant = restaurantService.addRestaurant(restaurantDTO);
        return new ResponseEntity<>(savedRestaurant, HttpStatus.CREATED);
    }

    // Admin: Add Food Item to Restaurant
    @PostMapping("/restaurants/{restaurantId}/food-items")
    public ResponseEntity<FoodItemDTO> addFoodItem(@PathVariable Long restaurantId, @Valid @RequestBody FoodItemDTO foodItemDTO) {
        FoodItemDTO savedFoodItem = restaurantService.addFoodItem(restaurantId, foodItemDTO);
        return new ResponseEntity<>(savedFoodItem, HttpStatus.CREATED);
    }

    // Admin: Update Restaurant Details
    @PutMapping("/restaurants/{id}")
    public ResponseEntity<RestaurantDTO> updateRestaurant(@PathVariable Long id, @Valid @RequestBody RestaurantDTO restaurantDTO) {
        RestaurantDTO updatedRestaurant = restaurantService.updateRestaurant(id, restaurantDTO);
        return new ResponseEntity<>(updatedRestaurant, HttpStatus.OK);
    }

    // Admin: Delete Restaurant
    @DeleteMapping("/restaurants/{id}")
    public ResponseEntity<ApiResponse> deleteRestaurant(@PathVariable Long id) {
        restaurantService.deleteRestaurant(id);
        return new ResponseEntity<>(new ApiResponse("Restaurant deleted successfully", LocalDateTime.now()), HttpStatus.OK);
    }

    // Admin: Delete Specific Food Item
    @DeleteMapping("/restaurants/{restaurantId}/food-items/{foodItemId}")
    public ResponseEntity<ApiResponse> deleteFoodItem(@PathVariable Long restaurantId, @PathVariable Long foodItemId) {
        restaurantService.deleteFoodItem(restaurantId, foodItemId);
        return new ResponseEntity<>(new ApiResponse("Food item deleted successfully", LocalDateTime.now()), HttpStatus.OK);
    }

    // Customer: View Single Restaurant (Excluding Menu)
    @GetMapping("/restaurants/{id}")
    public ResponseEntity<RestaurantDTO> getRestaurant(@PathVariable Long id) {
        RestaurantDTO restaurant = restaurantService.getRestaurantById(id);
        restaurant.setFoodItems(null); // Exclude menu
        return new ResponseEntity<>(restaurant, HttpStatus.OK);
    }

    // Customer: View All Restaurants (Excluding Menu)
    @GetMapping("/restaurants")
    public ResponseEntity<List<RestaurantDTO>> getAllRestaurants() {
        List<RestaurantDTO> restaurants = restaurantService.getAllRestaurants();
        restaurants.forEach(restaurant -> restaurant.setFoodItems(null)); // Exclude menu
        return new ResponseEntity<>(restaurants, HttpStatus.OK);
    }

    // Customer: View Selected Restaurant and Its Menu
    @GetMapping("/restaurants/{id}/menu")
    public ResponseEntity<RestaurantDTO> getRestaurantWithMenu(@PathVariable Long id) {
        RestaurantDTO restaurant = restaurantService.getRestaurantById(id);
        return new ResponseEntity<>(restaurant, HttpStatus.OK);
    }

    // Customer: Place Order
    @PostMapping("/orders")
    public ResponseEntity<OrderDTO> placeOrder(@Valid @RequestBody OrderDTO orderDTO) {
        OrderDTO savedOrder = orderService.placeOrder(orderDTO);
        return new ResponseEntity<>(savedOrder, HttpStatus.CREATED);
    }

    // Customer: Cancel Order
    @PutMapping("/orders/{id}/cancel")
    public ResponseEntity<ApiResponse> cancelOrder(@PathVariable Long id) {
        orderService.cancelOrder(id);
        return new ResponseEntity<>(new ApiResponse("Order cancelled successfully", LocalDateTime.now()), HttpStatus.OK);
    }

    // Admin/Customer: Display Orders by Status
    @GetMapping("/orders/status/{status}")
    public ResponseEntity<List<OrderDTO>> getOrdersByStatus(@PathVariable OrderStatus status) {
        List<OrderDTO> orders = orderService.getOrdersByStatus(status);
        return new ResponseEntity<>(orders, HttpStatus.OK);
    }

    // Admin: Update Order Status
    @PutMapping("/orders/{id}/status")
    public ResponseEntity<OrderDTO> updateOrderStatus(@PathVariable Long id, @RequestBody OrderStatus status) {
        OrderDTO updatedOrder = orderService.updateOrderStatus(id, status);
        return new ResponseEntity<>(updatedOrder, HttpStatus.OK);
    }
}

7. Centralized Exception Handling

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Map<String, String>> handleConstraintViolationException(ConstraintViolationException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getConstraintViolations().forEach(violation ->
            errors.put(violation.getPropertyPath().toString(), violation.getMessage())
        );
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
        return new ResponseEntity<>(new ApiResponse(ex.getMessage(), LocalDateTime.now()), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ApiResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
        return new ResponseEntity<>(new ApiResponse(ex.getMessage(), LocalDateTime.now()), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse> handleGenericException(Exception ex) {
        return new ResponseEntity<>(new ApiResponse("An unexpected error occurred", LocalDateTime.now()), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}