Skip to content

Spring Boot Project Setup and Layered Architecture

The Commercial Kitchen Analogy

Imagine walking into the kitchen of a world class restaurant. You do not see a single person chopping onions, cooking steaks, washing dishes, greeting diners, and taking orders simultaneously. That would create utter chaos.

Instead, the restaurant is organized into strict, specialized stations:

  1. Front of House (Waitstaff): Interacts with customers, takes orders, and delivers plated food. The waitstaff never cooks the food or buys the ingredients.
  2. Line Chefs (Preparation): Executes the recipes, combines ingredients, and applies business culinary rules (e.g. grilling to medium rare).
  3. Pantry & Cold Storage (Inventory): Stores and retrieves raw ingredients, keeping track of stock levels.

Spring Boot applications follow this exact Layered Architecture. The Controller Layer is your waitstaff: it receives HTTP requests, validates input, and returns formatted responses. The Service Layer is your kitchen line chef: it executes business logic, calculations, and transactional workflows. The Repository Layer is your pantry: it talks to the database, reading and writing records.

This lecture covers project initialization using Spring Initializr, directory structure conventions, the responsibilities of each layer, and why separating DTOs from database entities is essential for maintainability.


Setting Up a Spring Boot Project with Spring Initializr

The official and standard way to initialize a Spring Boot project is through Spring Initializr (start.spring.io).

[ Spring Initializr Configuration ]
  Project:         Maven
  Language:        Java
  Spring Boot:     3.2.x (Latest Stable)
  Group:           com.example
  Artifact:        order-service
  Packaging:       Jar
  Java Version:    17 or 21
  Dependencies:    Spring Web, Spring Data JPA, H2 Database / MySQL Driver, Lombok

Generated Directory Layout

order-service/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/com/example/orderservice/
│   │   │   ├── OrderServiceApplication.java   <-- Main Entry Point (@SpringBootApplication)
│   │   │   ├── controller/                    <-- Web Layer (REST endpoints)
│   │   │   ├── service/                       <-- Business Logic Layer
│   │   │   ├── repository/                    <-- Data Access Layer (Spring Data JPA)
│   │   │   ├── entity/                        <-- JPA Database Entities
│   │   │   ├── dto/                           <-- Data Transfer Objects
│   │   │   └── exception/                     <-- Custom Exceptions & Global Handler
│   │   └── resources/
│   │       ├── application.properties         <-- Configuration settings
│   │       └── static/ & templates/           <-- Static assets / HTML (if applicable)
│   └── test/                                  <-- Unit & Integration Tests

The Three Core Layers Explained

Spring Boot 3-Tier Layered Architecture

[ Client Request ]
       |
       v
+------------------+
| Controller Layer |  (@RestController)  Receives JSON, validates request, delegates to Service
+------------------+
       |
       v
+------------------+
|  Service Layer   |  (@Service)         Executes business rules, orchestrates transactions
+------------------+
       |
       v
+------------------+
| Repository Layer |  (@Repository)      Queries database, executes SQL / JPQL operations
+------------------+
       |
       v
+------------------+
|     Database     |  (PostgreSQL, MySQL, Oracle, H2)
+------------------+

1. Controller Layer (@RestController)

  • Role: Presentation and API boundary.
  • Responsibilities: Maps HTTP verbs (GET, POST, PUT, DELETE) and URL routes, validates incoming payload schemas (@Valid), extracts query parameters and headers, and serializes Java objects into HTTP responses.
  • Rule: Never place business rules or direct SQL queries in a controller.

2. Service Layer (@Service)

  • Role: Domain business logic.
  • Responsibilities: Calculations, discount rules, workflow orchestration, calling external third party APIs, security permission checks, and transaction boundaries (@Transactional).
  • Rule: Services should be decoupled from HTTP. A service method should be callable from a REST controller, a scheduled cron task, or a message listener without modification.

3. Repository Layer (@Repository)

  • Role: Persistence and data abstraction.
  • Responsibilities: CRUD operations, custom JPQL queries, pagination, and database mapping.
  • Rule: Repositories interact strictly with database entities and return data to the service layer.

Entity vs DTO: Why Separation Is Critical

A common beginner mistake is using @Entity database classes directly in REST controllers:

java
// BAD PRACTICE: Exposing database entity directly to public API
@PostMapping("/user")
public UserEntity createUser(@RequestBody UserEntity entity) {
    return userRepository.save(entity);
}

Why is this dangerous?

  1. Security Vulnerability (Over Posting): If UserEntity has an isAdmin or passwordHash field, a malicious user can include "isAdmin": true in their JSON payload. Spring will deserialize it directly into the entity, promoting the user to administrator.
  2. Tight Coupling: Any internal database schema change immediately breaks public API contracts for mobile apps and third party clients.
  3. Serialization Loops: Bidirectional JPA relationships (@OneToMany / @ManyToOne) cause infinite JSON recursion loops during Jackson serialization.

The Correct Pattern: Use DTOs (Data Transfer Objects)

java
// 1. DTO for API input
public class UserRegistrationRequest {
    @NotBlank
    private String username;

    @Email
    private String email;

    @Size(min = 8)
    private String password;

    // Getters and setters
}

// 2. Entity for Database persistence
@Entity
@Table(name = "users")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String email;
    private String passwordHash;
    private boolean isAdmin = false;

    // Getters and setters
}

The controller accepts UserRegistrationRequest, the service maps it to UserEntity (hashing the password and ensuring isAdmin defaults to false), and the repository saves the entity.


Complete End to End Flow

Here is the complete implementation across all three layers:

1. Repository

java
package com.example.orderservice.repository;

import com.example.orderservice.entity.OrderEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
    List<OrderEntity> findByCustomerId(String customerId);
}

2. Service

java
package com.example.orderservice.service;

import com.example.orderservice.dto.OrderRequest;
import com.example.orderservice.dto.OrderResponse;
import com.example.orderservice.entity.OrderEntity;
import com.example.orderservice.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public OrderResponse createOrder(OrderRequest request) {
        // Business logic: apply validation and calculations
        double finalAmount = request.getAmount();
        if (finalAmount > 500) {
            finalAmount = finalAmount * 0.90; // 10% discount
        }

        OrderEntity entity = new OrderEntity();
        entity.setCustomerId(request.getCustomerId());
        entity.setAmount(finalAmount);
        entity.setStatus("CONFIRMED");

        OrderEntity saved = orderRepository.save(entity);

        return new OrderResponse(saved.getId(), saved.getStatus(), saved.getAmount());
    }
}

3. Controller

java
package com.example.orderservice.controller;

import com.example.orderservice.dto.OrderRequest;
import com.example.orderservice.dto.OrderResponse;
import com.example.orderservice.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping
    public ResponseEntity<OrderResponse> placeOrder(@RequestBody OrderRequest request) {
        OrderResponse response = orderService.createOrder(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
}

Interview Questions & Pitfalls

Q1: What are the three primary layers in a standard Spring Boot application, and what is the responsibility of each?

The Controller layer handles incoming HTTP requests, input validation, and HTTP response formatting. The Service layer executes business logic, validation rules, external integrations, and transaction boundaries. The Repository layer abstracts database interactions and data persistence operations using JPA or JDBC.

Q2: Why should database entities never be exposed directly in controller endpoints?

Exposing entities creates security vulnerabilities (such as mass assignment or over posting of privileged fields), tightly couples the public API contract to internal database schemas, and frequently leads to infinite JSON serialization recursion with bidirectional JPA relationships. DTOs insulate the public contract from internal storage models.

Q3: What is the significance of the @SpringBootApplication annotation on the main class?

@SpringBootApplication is a meta annotation combining three essential annotations: @Configuration (marks the class as a configuration source for beans), @EnableAutoConfiguration (instructs Spring Boot to automatically configure beans based on classpath dependencies), and @ComponentScan (enables automatic package scanning for @Component, @Service, @Repository, and @RestController beans in the main class package and sub packages).

Q4: Can a service call another service, or can a repository call another repository?

Services frequently call other services to orchestrate complex business workflows. However, repositories should generally not call other repositories. Data orchestration belongs in the service layer, keeping repositories purely focused on single entity table operations.

Q5: What happens if your controller, service, and repository classes are in packages outside the main application package?

By default, @ComponentScan scans only the package where the @SpringBootApplication annotated class resides and its child sub packages. If your service or repository classes are placed in an external sibling package (e.g. com.other.service), Spring will fail to discover them at startup, throwing NoSuchBeanDefinitionException.