Spring Boot Fundamentals: Application Lifecycle
In this Spring Boot lesson, we will study Application Lifecycle through small executable cases. First we will make the normal result clear; then inspect data flow, ownership, failure behavior, and the caller contract before adding framework or infrastructure complexity.
Professional code is readable under change. A future developer should be able to identify which input is trusted, who may update state, where I/O happens, and how an expected error reaches a caller without reconstructing the whole application.
What You Will Build
20 focused Spring Boot examples for Application Lifecycle. Each has a visible result, an explicit contract, and a next change to test so the lesson becomes reusable knowledge rather than passive reading.
Example 1: Bootstrap one application
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@SpringBootApplication
public class CatalogApplication {
public static void main(String[] args) {
SpringApplication.run(CatalogApplication.class, args);
}
}Expected Result
The CatalogApplication context starts and owns the HTTP server
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 2: Declare focused dependencies
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
<project>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>Expected Result
The build includes Spring MVC and an embedded web server
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 3: Return a small REST representation
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@RestController
@RequestMapping("/api/topics")
class TopicController {
@GetMapping("/{id}")
TopicResponse find(@PathVariable UUID id) {
return new TopicResponse(id, "Spring Boot");
}
}
record TopicResponse(UUID id, String title) {}Expected Result
200 OK with a JSON id and title
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 4: Keep API DTOs separate from entities
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
record CreateTopicRequest(
@NotBlank String title,
@Size(max = 240) String summary
) {}
record TopicResponse(UUID id, String title) {}Expected Result
The API accepts only the fields it intentionally exposes
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 5: Use constructor injection
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@Service
class TopicService {
private final TopicRepository repository;
TopicService(TopicRepository repository) {
this.repository = repository;
}
}Expected Result
TopicService receives its repository explicitly and is easy to test
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 6: Model a persistent aggregate
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@Entity
class Topic {
@Id
@GeneratedValue
UUID id;
@Column(nullable = false, length = 120)
String title;
}Expected Result
JPA maps Topic to a table with a generated id and required title
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 7: Use a repository query
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
interface TopicRepository extends JpaRepository<Topic, UUID> {
Page<Topic> findByTitleContainingIgnoreCase(
String text,
Pageable pageable
);
}Expected Result
A paged title search is available without controller SQL
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 8: Keep a write inside one transaction
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@Transactional
public TopicResponse rename(UUID id, String title) {
Topic topic = repository.findById(id)
.orElseThrow(() -> new TopicNotFound(id));
topic.title = title;
return new TopicResponse(topic.id, topic.title);
}Expected Result
The read, validation, update, and commit succeed or fail together
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 9: Externalize runtime configuration
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
server:
port: 8080
app:
catalog:
page-size: 25
spring:
datasource:
url: ${DATABASE_URL}Expected Result
Port, page size, and datasource URL come from configuration
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 10: Bind typed configuration
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@ConfigurationProperties(prefix = "app.catalog")
@Validated
public record CatalogProperties(
@Min(1) int pageSize
) {}Expected Result
Invalid page-size configuration fails during startup
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 11: Validate a request at the boundary
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@PostMapping
ResponseEntity<TopicResponse> create(
@Valid @RequestBody CreateTopicRequest request
) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(service.create(request));
}Expected Result
Invalid input receives a deliberate 400 response before mutation
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 12: Return one intentional API error shape
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@RestControllerAdvice
class ApiErrors {
@ExceptionHandler(TopicNotFound.class)
ResponseEntity<ApiError> notFound(TopicNotFound error) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiError("TOPIC_NOT_FOUND", error.getMessage()));
}
}
record ApiError(String code, String message) {}Expected Result
A missing topic becomes a stable 404 API response
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 13: Protect routes with a security filter chain
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.build();
}Expected Result
Health is public; all remaining routes require authentication
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 14: Hash passwords instead of storing them
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories
.createDelegatingPasswordEncoder();
}
String stored = passwordEncoder().encode("replace-at-registration");Expected Result
The stored value is an algorithm-tagged password hash
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 15: Authorize close to the protected action
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@PreAuthorize("hasRole('EDITOR') or @topicAccess.owns(#id, authentication)")
public TopicResponse update(UUID id, UpdateTopicRequest request) {
return service.update(id, request);
}Expected Result
Only an editor or the resource owner may update the topic
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 16: Test a service rule quickly
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@ExtendWith(MockitoExtension.class)
class TopicServiceTest {
@Mock TopicRepository repository;
@Test
void rejectsMissingTopic() {
when(repository.findById(any())).thenReturn(Optional.empty());
assertThrows(TopicNotFound.class, () -> service.rename(UUID.randomUUID(), "New"));
}
}Expected Result
The domain failure is verified without starting Spring
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 17: Test the HTTP contract
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@WebMvcTest(TopicController.class)
class TopicControllerTest {
@Autowired MockMvc mvc;
@Test
void returnsTopic() throws Exception {
mvc.perform(get("/api/topics/{id}", UUID.randomUUID()))
.andExpect(status().isOk());
}
}Expected Result
The controller contract returns HTTP 200 for a valid route
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 18: Call a remote service through an explicit client
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@FeignClient(name = "catalog-search", path = "/api/search")
interface SearchClient {
@GetMapping
SearchResponse find(@RequestParam String query);
}
record SearchResponse(List<String> ids) {}Expected Result
The catalog service has a typed boundary to catalog-search
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 19: Contain remote failure
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
@CircuitBreaker(name = "search", fallbackMethod = "emptySearch")
public SearchResponse search(String query) {
return searchClient.find(query);
}
SearchResponse emptySearch(String query, Throwable error) {
return new SearchResponse(List.of());
}Expected Result
A failed dependency produces an intentional empty fallback result
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Example 20: Expose operational health without secrets
Let’s isolate one Spring Boot Fundamentals rule together. Run it, trace the inputs and output, then change one boundary value before reusing the pattern. Small observable programs turn syntax into a reliable engineering habit.
Keep contracts explicit: validate data before use, make ownership and mutation visible, and return a named result or failure a caller can test. Static types and a successful build help, but runtime boundaries still need intentional checks.
Spring Boot
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
probes:
enabled: trueExpected Result
Health, info, and metrics are available while sensitive endpoints stay closed
Try next: make the nearest empty, invalid, missing, or boundary case observable instead of leaving behavior to an unchecked assumption.
Design Review and Worked Trace
Review Spring Boot Fundamentals: Application Lifecycle as a full trace: input, validation, calculation or side effect, output, and cleanup. State the normal result, then state the boundary behavior. If either is hard to explain, split the work into a smaller function with a narrower contract.
Keep secret values and privileged decisions outside display code. Authentication establishes an identity; authorization decides what that identity may do. Check both close to protected operations, and use intentional failure responses rather than leaking internal details or crashing generically.
- Run the normal input and record the exact result.
- Test an empty, malformed, or boundary input deliberately.
- Keep state, ownership, and side effects visible to the caller.
- Return a result or named failure that can be tested.
- Never log credentials, tokens, or sensitive input.
This discipline makes Spring Boot code easier to review, safer to change, and more useful in production than a collection of untested syntax fragments.
Common Mistakes
- Skipping input validation because a local example happened to work.
- Hiding mutation, ownership, or failure behavior inside a convenient shortcut.
- Using a client-side display rule as the only protection for sensitive work.
- Copying code without changing an input to prove its contract.
