Career OS

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:

  1. Creates a single instance (singleton by default)
  2. Finds all constructor parameters
  3. Looks for beans of those types in its container
  4. 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:

  1. Sees JPA on the classpath
  2. Reads application.properties for database config
  3. Creates a DataSource bean
  4. Creates an EntityManagerFactory bean
  5. Creates a TransactionManager bean
  6. Scans for @Entity classes
  7. Scans for @Repository interfaces 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 @Autowired on 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

WhatWhereTime
DI explainedBaeldung “Intro to IoC and DI”15 min
Spring Boot auto-configSpring docs “Auto-configuration” section20 min
DTOs best practicesBaeldung “Entity to DTO Conversion”15 min
Testcontainerstestcontainers.com getting started30 min