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.
spring-boot-starter-web
spring-boot-starter-data-jpa
spring-boot-starter-validation
modelmapper
(version 3.2.3)@SpringBootApplication
class is the default base package for component scanning.@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
}
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
}
@Configuration
public class AppConfig {
@Bean
public ModelMapper modelMapper() {
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.STRICT)
.setPropertyCondition(Conditions.isNotNull());
return mapper;
}
}
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);
}
@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);
}
}
@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);
}
}
@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);
}
}