What is Spring Boot?
Spring Boot = Spring Framework + sensible defaults + zero XML config + embedded server. It takes the power of the Spring ecosystem and removes all the setup pain so you can write business logic from minute one.
Plain Spring Framework required hundreds of lines of XML or Java config just to get a REST endpoint running. Spring Boot uses "convention over configuration" — it guesses what you need based on what's on your classpath and auto-configures everything.
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
// That's it. Tomcat starts, REST endpoint is live. No XML. No web.xml.
Auto-Configuration
The magic behind Spring Boot. It scans your classpath and automatically creates beans you'd otherwise configure manually. Understanding this prevents "why did Spring create that?" confusion.
Auto-configuration works via @EnableAutoConfiguration (bundled inside @SpringBootApplication). Spring Boot has 130+ auto-configuration classes in spring-boot-autoconfigure.jar. Each one activates conditionally.
@Configuration
@ConditionalOnClass(DataSource.class) // only if DataSource is on classpath
@ConditionalOnMissingBean(DataSource.class) // only if YOU haven't defined one
public class DataSourceAutoConfiguration {
@Bean
public DataSource dataSource() {
return // creates HikariCP pool from application.properties
}
}
// ✅ When you add 'spring-boot-starter-data-jpa':
// → DataSource, EntityManagerFactory, TransactionManager — all auto-created
// ✅ When you define your OWN @Bean DataSource — auto-config backs off
Spring Boot Starters
Starters are curated dependency packs. Instead of figuring out which 8 libraries you need for JPA, you add one starter and get the right versions, all compatible.
| Starter | What it brings | Use when |
|---|---|---|
| starter-web | Spring MVC, Tomcat, Jackson (JSON) | Building REST APIs |
| starter-data-jpa | Hibernate, Spring Data JPA, HikariCP | SQL database access |
| starter-security | Spring Security, auth filters | Authentication & authorization |
| starter-test | JUnit 5, Mockito, AssertJ, MockMvc | Testing (always include) |
| starter-actuator | Health, metrics, env endpoints | Production monitoring |
| starter-validation | Hibernate Validator (Bean Validation) | Request body validation |
| starter-cache | Spring Cache abstraction + providers | Caching (with Redis/Caffeine) |
| starter-amqp | RabbitMQ / Spring AMQP | Message queues |
IoC Container
Inversion of Control = you don't create objects, Spring creates and manages them. The container (ApplicationContext) is the heart of Spring. It creates beans, injects dependencies, manages lifecycle.
Without IoC, you'd write UserService svc = new UserService(new UserRepo(new DataSource(...))) everywhere. With IoC, you declare what you need and Spring wires the whole graph automatically.
Dependency Injection
DI is how Spring gives beans their dependencies. Three ways to inject: constructor (best), setter, or field. You should almost always use constructor injection.
@Service
public class OrderService {
private final OrderRepository orderRepo;
private final PaymentService paymentSvc;
// Spring injects via constructor — no @Autowired needed in modern Spring
public OrderService(OrderRepository repo, PaymentService pmt) {
this.orderRepo = repo;
this.paymentSvc = pmt;
}
}
// ❌ AVOID — Field Injection (hard to test, hides dependencies)
@Autowired
private OrderRepository orderRepo; // bad — can't use final, can't mock easily
new OrderService(mockRepo, mockPayment) in unit tests without needing Spring context.
Bean Lifecycle & Scopes
Spring manages beans from creation to destruction. You can hook into lifecycle events. Bean scope defines how many instances get created.
| Scope | Instances Created | Use When |
|---|---|---|
| @Singleton (default) | One per application context | Stateless services, repositories — 99% of cases |
| @Prototype | New instance every injection | Stateful objects that shouldn't be shared |
| @RequestScope | One per HTTP request | Request-specific data (user context, request ID) |
| @SessionScope | One per HTTP session | User session data (shopping cart, preferences) |
public class CacheWarmupService {
@PostConstruct // runs after bean is fully initialized
public void warmupCache() {
// load frequently-accessed data into cache on startup
cacheRepo.loadProducts();
}
@PreDestroy // runs before bean is destroyed (app shutdown)
public void flushCache() {
cacheRepo.flush();
}
}
REST Controllers & Mappings
Spring MVC handles HTTP request routing, parameter binding, response serialization — all declaratively via annotations.
@RequestMapping("/api/products")
public class ProductController {
// GET /api/products?category=electronics&page=0
@GetMapping
public Page<ProductDTO> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(required = false) String category) {
return productService.findAll(page, category);
}
// GET /api/products/42
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getById(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// POST /api/products with JSON body
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ProductDTO create(@Valid @RequestBody CreateProductRequest req) {
return productService.create(req);
}
// DELETE /api/products/42
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
productService.delete(id);
}
}
Global Exception Handling
Instead of try-catch in every controller, use @ControllerAdvice to handle exceptions globally and return consistent error responses.
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult().getFieldErrors()
.stream().map(FieldError::getDefaultMessage).toList();
return new ErrorResponse("VALIDATION_FAILED", errors);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleAll(Exception ex) {
log.error("Unhandled exception", ex);
return new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
}
}
Bean Validation
Validate request bodies declaratively with annotations. Add @Valid in controller, annotate your DTO fields, Spring handles the rest.
@NotBlank(message = "Name is required")
@Size(min=2, max=50)
private String name;
@NotBlank
@Email(message = "Invalid email format")
private String email;
@NotNull
@Min(18) @Max(120)
private Integer age;
@Pattern(regexp = "^[6-9]\\d{9}$", message = "Invalid Indian mobile number")
private String phone;
}
JPA & Spring Data
Spring Data JPA eliminates 90% of boilerplate database code. Define an interface extending JpaRepository and get 50+ methods for free. Write zero SQL for common operations.
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Enumerated(EnumType.STRING)
private UserStatus status;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders;
}
// Repository — Spring generates ALL implementations
public interface UserRepository extends JpaRepository<User, Long> {
// Spring parses method names → generates SQL
Optional<User> findByEmail(String email);
List<User> findByStatusAndAgeGreaterThan(UserStatus status, int age);
Page<User> findByCityOrderByCreatedAtDesc(String city, Pageable p);
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
}
@Transactional
Wraps a method in a database transaction. If any exception occurs, everything rolls back atomically. One of Spring's most important features.
public class TransferService {
@Transactional // entire method = one transaction
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
accountRepo.debit(fromId, amount); // step 1
accountRepo.credit(toId, amount); // step 2
// if step 2 throws → step 1 auto-rolls back!
}
// Read-only for better performance (no dirty-checking, read replicas)
@Transactional(readOnly = true)
public List<Account> getAllAccounts() { ... }
// Requires a NEW transaction even if called within existing one
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditEvent(String event) { ... }
}
Configuration — application.properties & @Value
Externalize all config from code. Spring Boot reads application.properties (or .yml), maps values to beans, and makes everything overridable via environment variables.
server.port=8080
spring.application.name=my-api
# Database (auto-configures HikariCP + JPA)
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=postgres
spring.datasource.password=${DB_PASSWORD} # env variable
spring.jpa.hibernate.ddl-auto=validate
# Custom app config
app.jwt.secret=${JWT_SECRET}
app.jwt.expiry-minutes=60
app.max-file-size-mb=10
@ConfigurationProperties(prefix = "app.jwt")
@Component
public class JwtProperties {
private String secret;
private int expiryMinutes;
// getters/setters or use @Value + @ConfigurationPropertiesScan with records
}
// Inject and use:
public class JwtService {
private final JwtProperties jwtProps; // injected
public String generateToken(String sub) {
return Jwts.builder()
.setExpiration(...jwtProps.getExpiryMinutes()...)
.signWith(jwtProps.getSecret())
.compact();
}
}
Spring Profiles
Profiles let you have different configurations for dev, test, and prod. Spring loads the right config file based on the active profile.
application-dev.properties → dev overrides (H2 in-memory DB)
application-prod.properties → prod overrides (RDS PostgreSQL)
--- application-dev.properties ---
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.show-sql=true
logging.level.root=DEBUG
--- application-prod.properties ---
spring.datasource.url=jdbc:postgresql://${RDS_HOST}:5432/mydb
spring.jpa.show-sql=false
logging.level.root=WARN
--- Activate profile ---
$ java -jar app.jar --spring.profiles.active=prod
$ SPRING_PROFILES_ACTIVE=prod java -jar app.jar
Spring Security + JWT
Spring Security handles authentication and authorization. Modern approach uses JWT tokens — stateless, works perfectly with microservices and cloud deployments.
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // JWT = stateless, no CSRF needed
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // login/register open
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() // everything else needs JWT
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
Spring Boot Actuator
Actuator adds production-ready monitoring endpoints to your app instantly. Critical for Kubernetes health checks, AWS ALB, and integrating with Prometheus/Grafana.
| Endpoint | URL | Use |
|---|---|---|
/actuator/health | GET | K8s liveness/readiness & ALB health check |
/actuator/metrics | GET | JVM memory, GC, HTTP stats — for Prometheus |
/actuator/info | GET | App version, build info |
/actuator/env | GET | All config properties (secure in prod) |
/actuator/loggers | GET/POST | Change log level at runtime without restart |
/actuator/prometheus | GET | Prometheus-formatted metrics (add micrometer) |
Async Processing & @Scheduled
Run tasks asynchronously (non-blocking) or on a schedule — built directly into Spring Boot.
@EnableAsync
@EnableScheduling
public class MyApp { ... }
@Service
public class EmailService {
@Async // runs in separate thread — doesn't block HTTP response
public CompletableFuture<Void> sendWelcomeEmail(String email) {
// takes 2 seconds but caller returns immediately
mailSender.send(email);
return CompletableFuture.completedFuture(null);
}
}
@Component
public class ReportJob {
@Scheduled(cron = "0 0 8 * * MON-FRI") // 8 AM weekdays
public void generateDailyReport() {
reportService.generate();
}
@Scheduled(fixedDelay = 30000) // every 30 seconds
public void syncWithExternalService() { ... }
}
IoC Container → creates and wires all beans
@Service / @Repository / @Controller → declare beans
Constructor injection → preferred DI method
@RestController + @GetMapping → handle HTTP
@Transactional → atomic DB operations
JpaRepository → zero-boilerplate DB access
application.properties + Profiles → environment config
@ControllerAdvice → centralized error handling
Actuator → production health + metrics