Home Java Spring Boot Web App: From Starter to Production in 30 Minutes
Beginner 6 min · July 14, 2026

Spring Boot Web App: From Starter to Production in 30 Minutes

Build a production-ready web application with Spring Boot.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 25-30 min read
  • Basic knowledge of Java (Java 17 or later recommended)
  • Familiarity with Maven or Gradle build tools
  • Understanding of RESTful API concepts
  • An IDE (IntelliJ IDEA or Eclipse) installed
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use Spring Initializr to generate a starter project with spring-boot-starter-web.
  • Structure your code: controllers, services, repositories, DTOs.
  • Implement REST endpoints with proper HTTP methods and status codes.
  • Add global exception handling with @ControllerAdvice.
  • Write integration tests with @SpringBootTest and MockMvc.
  • Deploy with confidence using embedded Tomcat.
✦ Definition~90s read
What is Creating a Web Application with Spring Boot?

Spring Boot is a framework that simplifies building production-ready web applications by providing auto-configuration, embedded servers, and starter dependencies, so you can focus on business logic rather than boilerplate setup.

Think of Spring Boot as a pre-furnished apartment for your web app.
Plain-English First

Think of Spring Boot as a pre-furnished apartment for your web app. You don't need to build walls, plumbing, or electricity from scratch — Spring Boot provides the framework (like the apartment structure) so you can focus on decorating (your business logic). Just add your code and it's ready to live in (deploy).

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

I've been building Spring applications since 2008 — back when XML configuration was the norm and you'd spend days wiring beans. Spring Boot changed everything. It's not just a framework; it's a productivity revolution. But here's the hard truth: most tutorials show you how to create a "Hello World" app and call it a day. They don't tell you what happens when that app hits production and a NullPointerException brings down your entire service at 3 AM.

In this guide, I'll walk you through building a real web application from scratch — a payment processing service that handles customer transactions. You'll learn the starter setup, REST API design, error handling, testing, and deployment. Along the way, I'll share war stories from production outages I've debugged, so you avoid the same mistakes.

By the end, you'll have a production-ready Spring Boot application, not just a toy. You'll understand why @RestController is your friend, why global exception handling is non-negotiable, and why your tests should mock external services. Let's get our hands dirty.

1. Setting Up Your Spring Boot Starter

Let's start with the foundation. Use Spring Initializr (start.spring.io) — it's the only way to go. Select Maven or Gradle, Java 17 or 21 (I prefer 21 for virtual threads), and add dependencies: Spring Web, Spring Data JPA, H2 Database (for development), and Lombok.

Why these? Spring Web gives you the embedded Tomcat and REST support. Spring Data JPA handles database access. H2 is an in-memory database perfect for development and testing. Lombok reduces boilerplate — but be careful: Lombok can hide bugs if misused.

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

@SpringBootApplication public class PaymentApplication { public static void main(String[] args) { SpringApplication.run(PaymentApplication.class, args); } }

That's it. You have a running web server on port 8080. But don't stop here — real apps need structure.

PaymentApplication.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
package com.example.payment;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PaymentApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}
Output
Started PaymentApplication in 2.345 seconds (JVM running for 2.678)
💡Why not use the Spring Boot Maven plugin?
📊 Production Insight
I once saw a team add spring-boot-starter-webflux alongside spring-boot-starter-web. That caused classpath conflicts and weird startup errors. Stick to one reactive model — don't mix.
🎯 Key Takeaway
Spring Initializr generates a solid foundation. Add only the dependencies you need to keep your app lean.

2. Structuring Your Code: The Right Way

Package structure matters. Don't dump everything in one package. Use a layered architecture: controller, service, repository, dto, exception, config.

Here's what I've learned from debugging production issues: keep your controllers thin. They should only handle HTTP concerns — parsing requests, validating input, returning responses. Business logic belongs in services. Data access belongs in repositories.

Example structure: com.example.payment ├── controller │ └── PaymentController.java ├── service │ └── PaymentService.java ├── repository │ └── PaymentRepository.java ├── dto │ ├── PaymentRequest.java │ └── PaymentResponse.java ├── exception │ └── GlobalExceptionHandler.java ├── model │ └── Payment.java └── PaymentApplication.java

Why this matters? When you're debugging a 500 error at 2 AM, you want to know exactly where to look. This separation also makes testing easier — you can mock services without starting the full web context.

One more thing: use DTOs for your API layer. Don't expose your JPA entities directly. I've seen serialization issues, lazy loading exceptions, and security leaks because someone returned an entity with a password field. Always map entities to DTOs.

PaymentController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.example.payment.controller;

import com.example.payment.dto.PaymentRequest;
import com.example.payment.dto.PaymentResponse;
import com.example.payment.service.PaymentService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/payments")
public class PaymentController {

    private final PaymentService paymentService;

    public PaymentController(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @PostMapping
    public ResponseEntity<PaymentResponse> createPayment(@Valid @RequestBody PaymentRequest request) {
        PaymentResponse response = paymentService.processPayment(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    @GetMapping("/{id}")
    public ResponseEntity<PaymentResponse> getPayment(@PathVariable Long id) {
        PaymentResponse response = paymentService.getPayment(id);
        return ResponseEntity.ok(response);
    }

    @GetMapping
    public ResponseEntity<List<PaymentResponse>> getAllPayments() {
        List<PaymentResponse> payments = paymentService.getAllPayments();
        return ResponseEntity.ok(payments);
    }
}
Output
HTTP/1.1 201 Created
Content-Type: application/json
{"id":1,"amount":100.00,"currency":"USD","status":"COMPLETED"}
⚠ Never return entities directly
🎯 Key Takeaway
Use a layered architecture with DTOs. Keep controllers thin, services focused, and repositories simple.

3. Building RESTful Endpoints: The Right HTTP Methods and Status Codes

REST is not just about URLs — it's about using HTTP properly. I've seen countless APIs that return 200 for everything, or use POST for updates. Don't be that developer.

Here's the cheat sheet
  • POST /resources → Create (201 Created)
  • GET /resources → List (200 OK)
  • GET /resources/{id} → Read (200 OK)
  • PUT /resources/{id} → Full update (200 OK)
  • PATCH /resources/{id} → Partial update (200 OK)
  • DELETE /resources/{id} → Delete (204 No Content)

Use @RestController and @RequestMapping at the class level. Each method should have a specific mapping annotation: @GetMapping, @PostMapping, etc. This makes your code readable and self-documenting.

Always return ResponseEntity<T> for full control over status code and headers. For example, when creating a resource, return 201 with a Location header pointing to the new resource.

Let me show you a common mistake: returning a list directly from @GetMapping without wrapping it in ResponseEntity. That's fine, but you lose the ability to set headers. Use ResponseEntity for flexibility.

Another mistake: using @RequestMapping without specifying the method. That accepts all HTTP methods — a security risk. Always specify the method.

For more on this, check out our article on [REST PUT vs POST](/java/spring-rest-put-vs-post/).

PaymentController.java (updated)JAVA
1
2
3
4
5
6
7
8
9
10
11
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deletePayment(@PathVariable Long id) {
        paymentService.deletePayment(id);
        return ResponseEntity.noContent().build();
    }

    @PutMapping("/{id}")
    public ResponseEntity<PaymentResponse> updatePayment(@PathVariable Long id, @Valid @RequestBody PaymentRequest request) {
        PaymentResponse response = paymentService.updatePayment(id, request);
        return ResponseEntity.ok(response);
    }
Output
DELETE /api/payments/1 → 204 No Content
PUT /api/payments/1 → 200 OK with updated body
🔥Why 201 for creation?
📊 Production Insight
I once worked with a team that used POST for everything because 'it's simpler'. It broke every REST client and made caching impossible. Follow the spec — it's there for a reason.
🎯 Key Takeaway
Use the correct HTTP methods and status codes. Return ResponseEntity for control. Always specify the HTTP method in @RequestMapping.

4. Handling Errors Globally: Don't Let Your API Return Stack Traces

Default Spring Boot error handling returns a Whitelabel Error Page with a stack trace in development. In production, that's a security nightmare and a poor user experience. You must implement global exception handling.

Use @ControllerAdvice with @ExceptionHandler methods to catch exceptions and return consistent JSON error responses. Here's a pattern I've used in production:

public record ErrorResponse(int status, String message, long timestamp) {}

@ControllerAdvice public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNotFound(ResourceNotFoundException ex) { return new ErrorResponse(404, ex.getMessage(), System.currentTimeMillis()); }

@ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleValidation(MethodArgumentNotValidException ex) { String message = ex.getBindingResult().getFieldErrors().stream() .map(e -> e.getField() + ": " + e.getDefaultMessage()) .collect(Collectors.joining(", ")); return new ErrorResponse(400, message, System.currentTimeMillis()); }

@ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleGeneral(Exception ex) { // Log the exception log.error("Unexpected error", ex); return new ErrorResponse(500, "Internal server error", System.currentTimeMillis()); } }

Notice: we log the full stack trace for general exceptions but return a generic message to the client. Never expose internal details.

For a deep dive, see our guide on [Spring Boot exception handling](/java/spring-boot-exception-handling/).

GlobalExceptionHandler.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.payment.exception;

import com.example.payment.dto.ErrorResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse(404, ex.getMessage(), System.currentTimeMillis()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldErrors().stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .reduce((a, b) -> a + "; " + b)
                .orElse("Validation failed");
        return ResponseEntity.badRequest()
                .body(new ErrorResponse(400, message, System.currentTimeMillis()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        log.error("Unexpected error", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse(500, "An unexpected error occurred", System.currentTimeMillis()));
    }
}
Output
{"status":404,"message":"Payment not found with id 123","timestamp":1712345678000}
⚠ Don't catch exceptions too broadly
📊 Production Insight
A client once told me their API returned 'null' in the error message. They were returning ex.getMessage() which was null for a NullPointerException. Always provide a fallback message.
🎯 Key Takeaway
Implement @ControllerAdvice for global exception handling. Return consistent JSON error responses. Log full stack traces server-side but expose only generic messages to clients.

5. Validation: Never Trust Client Input

Input validation is your first line of defense. Use Bean Validation annotations (@NotNull, @Size, @Email, etc.) on your DTOs. Then add @Valid in your controller method.

But here's what the docs don't tell you: validation annotations on entities are not enough. You need to validate at the API boundary. Also, custom validation logic often requires cross-field validation — use @ScriptAssert or implement a custom validator.

public record PaymentRequest( @NotNull @DecimalMin("0.01") BigDecimal amount, @NotNull Currency currency, @NotNull @Size(min = 3, max = 100) String description ) {}

public ResponseEntity<PaymentResponse> createPayment(@Valid @RequestBody PaymentRequest request)

If validation fails, Spring throws MethodArgumentNotValidException, which we handle in our global handler.

For more complex scenarios, consider groups or custom validators. But keep it simple: most validation can be done with built-in annotations.

Check out our article on [Spring Boot validation](/java/spring-boot-validation/) for advanced techniques.

PaymentRequest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.payment.dto;

import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.util.Currency;

public record PaymentRequest(
    @NotNull(message = "Amount is required")
    @DecimalMin(value = "0.01", message = "Amount must be at least 0.01")
    BigDecimal amount,

    @NotNull(message = "Currency is required")
    Currency currency,

    @NotBlank(message = "Description is required")
    @Size(max = 255, message = "Description must be at most 255 characters")
    String description
) {}
💡Use records for DTOs
📊 Production Insight
I once saw a production issue where a client sent an empty string for a required field. The validation passed because they used @Size(min=0) instead of @NotBlank. Always use @NotBlank for strings.
🎯 Key Takeaway
Always validate input at the API boundary with Bean Validation annotations. Use @Valid in controller methods. Handle validation errors globally.

6. Testing Your Web Layer: Beyond Unit Tests

Unit tests are great for services, but for controllers you need integration tests that actually start the web context. Use @WebMvcTest for slicing — it starts only the web layer, not the full application.

@WebMvcTest(PaymentController.class) class PaymentControllerTest {

@Autowired private MockMvc mockMvc;

@MockBean private PaymentService paymentService;

@Test void createPayment_shouldReturn201() throws Exception { PaymentRequest request = new PaymentRequest(new BigDecimal("100.00"), Currency.getInstance("USD"), "Test payment"); PaymentResponse response = new PaymentResponse(1L, new BigDecimal("100.00"), "USD", "COMPLETED"); when(paymentService.processPayment(any())).thenReturn(response);

mockMvc.perform(post("/api/payments") .contentType(MediaType.APPLICATION_JSON) .content("{\"amount\":100.00,\"currency\":\"USD\",\"description\":\"Test payment\"}")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.status").value("COMPLETED")); } }

Notice: we mock the service layer. This test verifies HTTP mapping, serialization, and validation — without hitting the database.

For full integration tests, use @SpringBootTest with TestContainers to spin up a real database. This catches issues like wrong column mappings or SQL syntax errors.

See our guides on [Spring Boot testing with JUnit & Mockito](/java/spring-boot-testing-junit-mockito/) and [advanced testing](/java/spring-boot-testing-advanced/).

PaymentControllerTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.payment.controller;

import com.example.payment.dto.PaymentRequest;
import com.example.payment.dto.PaymentResponse;
import com.example.payment.service.PaymentService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.math.BigDecimal;
import java.util.Currency;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(PaymentController.class)
class PaymentControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private PaymentService paymentService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void createPayment_shouldReturn201() throws Exception {
        PaymentRequest request = new PaymentRequest(new BigDecimal("100.00"), Currency.getInstance("USD"), "Test payment");
        PaymentResponse response = new PaymentResponse(1L, new BigDecimal("100.00"), "USD", "COMPLETED");
        when(paymentService.processPayment(any())).thenReturn(response);

        mockMvc.perform(post("/api/payments")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.status").value("COMPLETED"));
    }
}
Output
Test passed: Status 201, JSON path $.status = COMPLETED
🔥Why MockMvc over TestRestTemplate?
📊 Production Insight
I once debugged a test that passed locally but failed in CI. The issue was a different default locale causing number formatting differences. Always set a fixed locale in tests.
🎯 Key Takeaway
Use @WebMvcTest for controller tests with MockMvc. Mock dependencies. Test HTTP status, response body, and validation errors.

What the Official Docs Won't Tell You

Spring Boot documentation is excellent, but it glosses over real-world pitfalls. Here are things I've learned the hard way:

  1. Classpath scanning can bite you: If you have multiple @SpringBootApplication classes (e.g., in tests), Spring Boot might scan the wrong one. Always specify the main class in @SpringBootTest(classes = PaymentApplication.class).
  2. Embedded Tomcat configuration: The docs show server.port=8080, but they don't mention that you should also configure server.tomcat.max-threads, server.tomcat.max-connections, and server.tomcat.accept-count for production. Defaults are too low.
  3. Graceful shutdown: By default, Spring Boot kills the application immediately on SIGTERM. Add server.shutdown=graceful and set a timeout. Otherwise, in-flight requests get interrupted.
  4. Logging configuration: The default logback configuration logs to console only. In production, you need file appenders with rotation. Use logback-spring.xml and set logging.file.name.
  5. Actuator endpoints: Exposing /actuator/health is fine, but /actuator/env or /actuator/beans can leak sensitive info. Secure them with Spring Security or restrict to internal networks.
  6. @Transactional pitfalls: Adding @Transactional on a controller method is a common mistake. It opens a transaction for the entire request, which can cause long database locks. Keep transactions at the service layer.
  7. Lazy initialization: Spring Boot 2.2+ supports spring.main.lazy-initialization=true. It speeds up startup but can mask bean creation errors until first use. Use it with caution.

These are the gotchas that cause production outages. Don't ignore them.

⚠ Lazy initialization and circular dependencies
📊 Production Insight
I once had a production outage because the default Tomcat max-threads (200) was too low for Black Friday traffic. We saw connection timeouts and 503s. Always tune your thread pool based on load testing.
🎯 Key Takeaway
The docs cover the happy path. Real-world production requires configuring Tomcat, graceful shutdown, logging, and security. Always test with production-like settings.

7. Deploying to Production: From JAR to Live

Building is easy: mvn clean package produces a fat JAR. But deployment is where many apps fail.

First, create an application-prod.yml with production settings: database connection, logging, server config. Use environment variables for secrets. Never hardcode passwords.

server: port: ${PORT:8080} tomcat: max-threads: 200 max-connections: 10000 accept-count: 100 shutdown: graceful

spring: datasource: url: ${DATABASE_URL} username: ${DATABASE_USER} password: ${DATABASE_PASSWORD} jpa: hibernate: ddl-auto: validate # Never use update in production

logging: file: name: /var/log/app/app.log level: root: WARN com.example: INFO

Run with: java -jar app.jar --spring.profiles.active=prod

Consider using a process manager like systemd or a container orchestration platform like Kubernetes. Always set up health checks on /actuator/health.

For API documentation, integrate SpringDoc OpenAPI (see [Spring Boot Swagger/OpenAPI](/java/spring-boot-swagger-openapi/)). It generates interactive API docs from your code.

Finally, monitor with Spring Boot Actuator and Micrometer. Track request rates, error rates, and latency. Set up alerts for 5xx errors.

Deployment is not the end — it's the beginning of operations.

application-prod.ymlJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server:
  port: ${PORT:8080}
  tomcat:
    max-threads: 200
    max-connections: 10000
    accept-count: 100
  shutdown: graceful

spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DATABASE_USER}
    password: ${DATABASE_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

logging:
  file:
    name: /var/log/app/app.log
  pattern:
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
  level:
    root: WARN
    com.example: INFO
💡Use ddl-auto=validate in production
📊 Production Insight
I once saw a team deploy with ddl-auto=update and it dropped a column because the entity was missing a field. They lost data. Always use validate and manage schema migrations separately.
🎯 Key Takeaway
Use profile-specific configuration. Never hardcode secrets. Tune Tomcat for production load. Use ddl-auto=validate. Monitor with Actuator.
● Production incidentPOST-MORTEMseverity: high

The Case of the Silent 500: When a Missing @ResponseBody Crashed a Payment Gateway

Symptom
All POST requests to /api/payments returned HTTP 500 with an empty body. The client saw 'Internal Server Error' with no details.
Assumption
The developer assumed it was a database connection issue or a bug in the payment gateway integration.
Root cause
The controller method was annotated with @Controller instead of @RestController, and the method returned a PaymentResponse object. Spring MVC tried to resolve a view named 'PaymentResponse', failed, and threw an InternalResourceViewResolver exception that was swallowed by the default error handling.
Fix
Changed @Controller to @RestController (or added @ResponseBody to each method). The response was then serialized to JSON correctly.
Key lesson
  • Always use @RestController for REST APIs — it combines @Controller and @ResponseBody.
  • Configure a global exception handler with @ControllerAdvice to catch and log all exceptions.
  • Enable detailed error responses in development (server.error.include-stacktrace=always) and sanitize them in production.
  • Test your endpoints with actual HTTP clients, not just unit tests.
  • Log the full stack trace on 500 errors — don't rely on default error pages.
Production debug guideSymptom to Action5 entries
Symptom · 01
Endpoint returns 404 for a path you defined
Fix
Check your controller mapping. Use 'mvn spring-boot:run -Ddebug' to see request mappings at startup. Verify the path is correct and the method is public.
Symptom · 02
Request body is null or empty
Fix
Ensure you have @RequestBody on the parameter. Check that the Content-Type header is 'application/json'. If using DTOs, verify they have a no-arg constructor and setters.
Symptom · 03
JSON serialization fails (e.g., infinite recursion)
Fix
Add @JsonIgnore or @JsonManagedReference/@JsonBackReference on bidirectional relationships. Use @JsonView to control what gets serialized.
Symptom · 04
500 error with no logs
Fix
Add a global exception handler with @ControllerAdvice. Log the exception. Check if your logging framework is configured correctly (log levels, appenders).
Symptom · 05
Slow response times
Fix
Profile with Spring Boot Actuator and Micrometer. Check database queries, external API calls, and thread pool usage. Enable HTTP request logging with a filter.
★ Quick Debug Cheat SheetImmediate actions for common Spring Boot web app issues
Endpoint returns 404
Immediate action
Check application logs for request mappings
Commands
curl -v http://localhost:8080/your-path
grep 'RequestMapping' /var/log/app.log
Fix now
Ensure @RequestMapping or @GetMapping annotation is correct and the controller is scanned (component scan).
Request body is null+
Immediate action
Check HTTP headers and DTO structure
Commands
curl -v -H "Content-Type: application/json" -d '{"key":"value"}' http://localhost:8080/api
check DTO has no-arg constructor and setters
Fix now
Add @RequestBody to parameter and ensure DTO is properly structured.
500 error with no stack trace+
Immediate action
Add global exception handler
Commands
Check application logs for 'ERROR' level
Add @ControllerAdvice class with @ExceptionHandler methods
Fix now
Implement a global exception handler that logs the exception and returns a consistent error response.
Feature@Controller@RestController
PurposeMVC controller for viewsREST controller for JSON/XML
Return valueView name (resolved by ViewResolver)Object (serialized directly)
Requires @ResponseBodyYes, on each methodNo (built-in)
Common useWeb pages with Thymeleaf/JSPREST APIs and microservices
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
PaymentApplication.java@SpringBootApplication1. Setting Up Your Spring Boot Starter
PaymentController.java@RestController2. Structuring Your Code
PaymentController.java (updated)@DeleteMapping("/{id}")3. Building RESTful Endpoints
GlobalExceptionHandler.java@ControllerAdvice4. Handling Errors Globally
PaymentRequest.javapublic record PaymentRequest(5. Validation
PaymentControllerTest.java@WebMvcTest(PaymentController.class)6. Testing Your Web Layer
application-prod.ymlserver:7. Deploying to Production

Key takeaways

1
Use Spring Initializr to generate a project with spring-boot-starter-web. Structure code in layers
controller, service, repository, dto.
2
Always use @RestController for REST APIs. Return ResponseEntity with proper HTTP status codes (201 for creation, 204 for delete).
3
Implement global exception handling with @ControllerAdvice. Return consistent JSON error responses.
4
Validate input with Bean Validation annotations and @Valid. Handle validation errors globally.
5
Write integration tests with @WebMvcTest and MockMvc. Mock service dependencies.
6
Configure production settings
Tomcat tuning, graceful shutdown, logging, and ddl-auto=validate.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the Spring Boot auto-configuration mechanism. How does it decide...
Q02JUNIOR
How would you implement a global exception handler for a REST API in Spr...
Q03SENIOR
What is the difference between @MockBean and @Mock in Spring Boot tests?
Q01 of 03SENIOR

Explain the Spring Boot auto-configuration mechanism. How does it decide which beans to configure?

ANSWER
Spring Boot auto-configuration uses @EnableAutoConfiguration and spring.factories files to load auto-configuration classes. These classes use @ConditionalOnClass, @ConditionalOnMissingBean, and other conditional annotations to decide whether to configure a bean based on the classpath, existing beans, and properties. For example, if H2 is on the classpath and no DataSource bean is defined, it auto-configures an H2 in-memory database.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between @Controller and @RestController?
02
How do I handle validation errors in Spring Boot?
03
Should I use @SpringBootTest or @WebMvcTest for testing controllers?
04
How do I configure CORS in Spring Boot?
05
What is the best way to manage database schema changes?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

6 min read · try the examples if you haven't

Previous
Spring WebClient vs RestTemplate: When to Use Each in Modern Spring Applications
121 / 121 · Spring Boot
Next
Introduction to Hibernate ORM