Week 4 — Spring Boot: Understand the Magic
The textbook for this week
This page is the schedule; the actual teaching lives in the Spring Boot track. Day 1 (DI) = module 01, Day 2 (auto-config) = module 02, Day 3 (DTOs + validation) = module 05, Day 4 (service layer) = module 06, Day 5 (testing) = module 08. Work the modules; use this page as the weekly checklist.
Why “Deep” and Not Just “Use”
Anyone can follow a Spring Boot tutorial. AI can generate Spring Boot code. What makes you valuable is understanding what Spring is doing behind @SpringBootApplication — because when it breaks (and it will), you need to debug the framework, not just your code.
Learning Goals
By Friday:
- Explain dependency injection without mentioning Spring (it’s a design pattern, not a framework feature)
- Understand what auto-configuration does and how to override it
- Implement validation, DTOs, and proper service-layer patterns
- Have a fully functional CRUD API with error handling
Daily Breakdown
Day 1: Dependency Injection — The Concept
Before Spring, you’d write:
public class ExpenseService {
private ExpenseRepository repo = new ExpenseRepository(); // tightly coupled
private NotificationService notifier = new NotificationService(); // hard to test
}
Problem: ExpenseService creates its own dependencies. You can’t swap them for testing. You can’t change behavior without editing this class.
DI solution (no framework needed):
public class ExpenseService {
private final ExpenseRepository repo;
private final NotificationService notifier;
// Dependencies injected from outside
public ExpenseService(ExpenseRepository repo, NotificationService notifier) {
this.repo = repo;
this.notifier = notifier;
}
}
Spring just automates this. When you put @Service on a class, Spring:
- Creates a single instance (singleton by default)
- Finds all constructor parameters
- Looks for beans of those types in its container
- Injects them automatically
That’s all @Autowired does. It’s not magic. It’s constructor injection automated.
Day 2: Auto-Configuration — How Spring Boot “Just Works”
What happens when you add spring-boot-starter-data-jpa to your pom.xml?
Spring Boot:
- Sees JPA on the classpath
- Reads
application.propertiesfor database config - Creates a
DataSourcebean - Creates an
EntityManagerFactorybean - Creates a
TransactionManagerbean - Scans for
@Entityclasses - Scans for
@Repositoryinterfaces and creates implementations
All automatic. But you need to know this because:
- When the DataSource config is wrong, you need to know what bean to debug
- When you need two databases, you need to override auto-config
- When tests need a different database, you need to understand profiles
Exercise: Add spring.jpa.show-sql=true and logging.level.org.springframework=DEBUG to your properties. Watch what Spring does on startup. Read the logs.
Day 3: Validation + DTOs
Never expose your entities directly. This is a security and maintainability rule.
// Request DTO — what the client sends
public record CreateExpenseRequest(
@NotBlank String description,
@NotNull @Positive BigDecimal amount,
@NotNull Long paidByUserId,
@NotEmpty List<SplitRequest> splits
) {}
public record SplitRequest(
@NotNull Long userId,
@NotNull @Positive BigDecimal amount
) {}
// Response DTO — what the client receives
public record ExpenseDTO(
Long id,
String description,
BigDecimal amount,
String paidByName,
LocalDateTime createdAt,
List<SplitDTO> splits
) {}
Why DTOs?
- Entities have JPA annotations, relationships, lazy-loading — clients don’t need that
- You control exactly what data goes in and out
- You can evolve your DB schema without breaking the API
- Security: you don’t accidentally expose internal fields
Add validation to your project today. Make invalid requests return clean 400 errors.
Day 4: Service Layer — Business Logic Lives Here
@Service
@Transactional
public class ExpenseService {
public ExpenseDTO createExpense(Long groupId, CreateExpenseRequest request) {
// 1. Validate group exists
Group group = groupRepo.findById(groupId)
.orElseThrow(() -> new GroupNotFoundException(groupId));
// 2. Validate payer is in the group
User payer = userRepo.findById(request.paidByUserId())
.orElseThrow(() -> new UserNotFoundException(request.paidByUserId()));
if (!group.getMembers().contains(payer)) {
throw new UserNotInGroupException(payer.getId(), groupId);
}
// 3. Validate splits add up to total amount
BigDecimal splitTotal = request.splits().stream()
.map(SplitRequest::amount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (splitTotal.compareTo(request.amount()) != 0) {
throw new SplitMismatchException(request.amount(), splitTotal);
}
// 4. Create expense and splits
// 5. Return DTO
}
}
Notice: The business rules (payer must be in group, splits must equal total) are the hard part. The CRUD is trivial. This is what makes you valuable.
Day 5: Integration Testing
@SpringBootTest
@AutoConfigureMockMvc
class ExpenseControllerTest {
@Autowired MockMvc mockMvc;
@Autowired ObjectMapper objectMapper;
@Test
void createExpense_validRequest_returns201() throws Exception {
var request = new CreateExpenseRequest(
"Dinner", new BigDecimal("300.00"), 1L,
List.of(new SplitRequest(1L, new BigDecimal("150.00")),
new SplitRequest(2L, new BigDecimal("150.00")))
);
mockMvc.perform(post("/api/v1/groups/1/expenses")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.data.description").value("Dinner"));
}
@Test
void createExpense_splitsMismatch_returns400() throws Exception {
// Splits don't add up to amount — should fail
}
}
Test against a real database (use Testcontainers or a test PostgreSQL instance). Not H2. Not mocks.
Weekend: Month 1 Checkpoint
Your project should now have:
- User, Group, Expense, ExpenseSplit entities with proper JPA relationships
- REST endpoints for all CRUD operations
- DTOs for all request/response objects
- Validation on all inputs
- Custom exception hierarchy + global error handler
- Service layer with real business logic (split validation, group membership checks)
- At least 5 integration tests
- PostgreSQL database (not H2)
- Clean code — no
@Autowiredon fields (use constructor injection) - DSA: DSA 02 — Arrays & Hashing done + 5 problems from its list (Two Sum from memory before you move on)
If you have all this, you’ve built more than most bootcamp graduates build in 3 months. And you understand WHY it works.
Resources
| What | Where | Time |
|---|---|---|
| DI explained | Baeldung “Intro to IoC and DI” | 15 min |
| Spring Boot auto-config | Spring docs “Auto-configuration” section | 20 min |
| DTOs best practices | Baeldung “Entity to DTO Conversion” | 15 min |
| Testcontainers | testcontainers.com getting started | 30 min |