Spring Boot is the dominant framework for building Java backend applications — powering APIs, microservices, and enterprise systems at companies like Netflix, Airbnb, and LinkedIn. This roadmap shows you exactly what to learn, in what order, and realistic timelines to go from zero to a job-ready Spring Boot developer.
At a glance
| Phase |
Topics |
Time estimate |
| 1 |
Java fundamentals — syntax, OOP, collections |
6–8 weeks |
| 2 |
Spring Core — IoC, DI, beans, AOP |
3–4 weeks |
| 3 |
Spring Boot basics — auto-config, starter POMs |
2–3 weeks |
| 4 |
Building REST APIs — controllers, validation, error handling |
4–5 weeks |
| 5 |
Data access — JPA, Hibernate, Spring Data |
4–6 weeks |
| 6 |
Security — Spring Security, JWT, OAuth2 |
3–4 weeks |
| 7 |
Testing — JUnit 5, Mockito, Testcontainers |
3–4 weeks |
| 8 |
Messaging & async — Kafka, RabbitMQ |
2–3 weeks |
| 9 |
Microservices — Spring Cloud, service mesh |
4–6 weeks |
| 10 |
DevOps — Docker, CI/CD, AWS/GCP/Azure |
3–4 weeks |
| Total to first job |
|
~12–18 months |
Phase 1 — Java fundamentals (Weeks 1–8)
Spring Boot runs on Java. You need solid language fundamentals before touching any framework.
Core Java concepts to master
| Concept |
Key details |
| JDK vs JRE vs JVM |
JDK = compile + run; JRE = run only; JVM = bytecode execution engine |
| Data types |
Primitives: int, long, double, boolean; Wrappers: Integer, Long, Double |
| String |
Immutable; String.format(), StringBuilder for concatenation in loops |
| Collections |
ArrayList, LinkedList, HashMap, HashSet, TreeMap — know time complexity |
| Generics |
List<T>, Map<K,V>, bounded wildcards <? extends Number> |
| Streams & Lambdas |
list.stream().filter().map().collect() — essential for modern Java |
| Optional |
Replace null checks with Optional.ofNullable().orElse() |
| Exception handling |
Checked vs unchecked, try-catch-finally, custom exceptions |
| Concurrency |
Thread, ExecutorService, CompletableFuture, synchronized |
OOP pillars you must know
| Pillar |
Spring Boot use |
| Encapsulation |
Private fields + public getters/setters in entity classes |
| Inheritance |
Base BaseEntity class with id, createdAt, updatedAt |
| Polymorphism |
Service interfaces with multiple implementations |
| Abstraction |
Abstract base classes, strategy pattern in services |
Minimal Java example
// Modern Java: streams + optionals + records
public record UserDto(Long id, String name, String email) {}
List<UserDto> activeUsers = users.stream()
.filter(User::isActive)
.map(u -> new UserDto(u.getId(), u.getName(), u.getEmail()))
.toList(); // Java 16+
Phase 2 — Spring Core (Weeks 9–12)
Spring Boot is built on Spring Framework. Understand the core before relying on auto-configuration magic.
Inversion of Control (IoC) & Dependency Injection
| Concept |
What it means |
| IoC container |
Spring manages object lifecycle — you don't call new manually |
| DI |
Objects receive their dependencies from the container, not create them |
| Bean |
Any object managed by the Spring container |
@Component |
Generic stereotype — marks a class as a Spring bean |
@Service |
Specialization of @Component for business logic layer |
@Repository |
Specialization for data access — adds exception translation |
@Controller |
Specialization for web layer |
@Autowired |
Injects a bean (prefer constructor injection over field injection) |
Constructor injection (preferred)
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final EmailService emailService;
// Constructor injection — testable, immutable
public OrderService(OrderRepository orderRepository, EmailService emailService) {
this.orderRepository = orderRepository;
this.emailService = emailService;
}
}
Bean scopes
| Scope |
Lifecycle |
Use case |
singleton (default) |
One instance per container |
Stateless services |
prototype |
New instance per request |
Stateful objects |
request |
One instance per HTTP request |
Web request data |
session |
One instance per HTTP session |
User session data |
Aspect-Oriented Programming (AOP)
@Aspect
@Component
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
@Around("execution(* com.example.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
log.info("{} executed in {}ms", joinPoint.getSignature(), elapsed);
return result;
}
}
Phase 3 — Spring Boot basics (Weeks 13–15)
Spring Boot removes boilerplate through auto-configuration and opinionated defaults.
Key Spring Boot concepts
| Feature |
What it does |
@SpringBootApplication |
Combines @Configuration + @EnableAutoConfiguration + @ComponentScan |
| Auto-configuration |
Detects classpath libraries and configures them automatically |
| Starter POMs |
Curated dependency sets: spring-boot-starter-web, spring-boot-starter-data-jpa |
| Embedded server |
Tomcat (default), Jetty, or Undertow — no WAR deployment needed |
application.properties |
Central config file; application.yml is the YAML alternative |
| Profiles |
spring.profiles.active=dev — different config per environment |
| Actuator |
Production-ready endpoints: /actuator/health, /actuator/metrics, /actuator/info |
Project structure (standard)
src/
├── main/
│ ├── java/com/example/
│ │ ├── Application.java # @SpringBootApplication
│ │ ├── controller/ # @RestController classes
│ │ ├── service/ # @Service classes + interfaces
│ │ ├── repository/ # @Repository + JPA interfaces
│ │ ├── model/ # @Entity classes
│ │ ├── dto/ # Data Transfer Objects
│ │ ├── config/ # @Configuration classes
│ │ └── exception/ # Custom exceptions + handlers
│ └── resources/
│ ├── application.yml # Config
│ ├── application-dev.yml # Dev profile
│ └── application-prod.yml # Prod profile
└── test/
└── java/com/example/ # Test classes mirror main
application.yml example
spring:
application:
name: my-api
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USER}
password: ${DB_PASS}
jpa:
hibernate:
ddl-auto: validate # use migrations in prod, not create/update
show-sql: false
server:
port: 8080
management:
endpoints:
web:
exposure:
include: health,info,metrics
Phase 4 — Building REST APIs (Weeks 16–20)
This is the core skill. Learn to build clean, well-validated, properly documented REST APIs.
REST controller anatomy
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public Page<UserDto> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userService.findAll(PageRequest.of(page, size));
}
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto createUser(@Valid @RequestBody CreateUserRequest request) {
return userService.create(request);
}
@PutMapping("/{id}")
public UserDto updateUser(@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
}
Validation with Bean Validation
public record CreateUserRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
String name,
@Email(message = "Valid email required")
@NotBlank
String email,
@NotBlank
@Size(min = 8, message = "Password must be at least 8 characters")
String password,
@Min(0) @Max(150)
int age
) {}
Global exception handling
@RestControllerAdvice
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(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
return new ErrorResponse("VALIDATION_ERROR", String.join(", ", errors));
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception ex) {
log.error("Unhandled exception", ex);
return new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred");
}
}
API versioning strategies
| Strategy |
Example |
Pros |
Cons |
| URI versioning |
/api/v1/users |
Simple, cacheable |
URL pollution |
| Header versioning |
Accept: application/vnd.api.v1+json |
Clean URLs |
Less discoverable |
| Query param |
/users?version=1 |
Easy to test |
Cache issues |
| Recommended |
URI versioning |
Industry standard |
— |
Phase 5 — Data access (Weeks 21–26)
Spring Data JPA + Hibernate
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(unique = true, nullable = false)
private String email;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
}
Repository layer
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
@Query("SELECT u FROM User u WHERE u.createdAt > :date AND u.active = true")
List<User> findActiveUsersCreatedAfter(@Param("date") LocalDateTime date);
// Pagination built in
Page<User> findByNameContainingIgnoreCase(String name, Pageable pageable);
}
Database migration with Flyway
-- V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- V2__add_users_index.sql
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
JPA N+1 problem (critical to understand)
// BAD: N+1 — 1 query for users + N queries for each user's orders
List<User> users = userRepository.findAll();
users.forEach(u -> u.getOrders().size()); // triggers N lazy loads
// GOOD: fetch join — 1 query total
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders();
Database choice guide
| Database |
Spring Boot starter |
Use case |
| PostgreSQL |
spring-boot-starter-data-jpa + postgresql |
General purpose, JSON support |
| MySQL |
spring-boot-starter-data-jpa + mysql-connector-j |
Popular, wide hosting support |
| MongoDB |
spring-boot-starter-data-mongodb |
Document storage, flexible schema |
| Redis |
spring-boot-starter-data-redis |
Caching, sessions, pub/sub |
| H2 |
h2 |
In-memory for testing only |
Phase 6 — Security (Weeks 27–30)
Spring Security fundamentals
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // stateless REST API
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
JWT authentication
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration; // milliseconds
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignKey())
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private SecretKey getSignKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
}
Security concepts checklist
| Topic |
What to know |
| Authentication vs Authorization |
AuthN = who are you; AuthZ = what can you do |
| BCrypt |
Use for password hashing — never store plain text |
| JWT |
Header.Payload.Signature — stateless auth token |
| OAuth2 |
Delegate auth to Google/GitHub/Okta — spring-security-oauth2-client |
| CORS |
Configure allowed origins for frontend-backend separation |
| HTTPS |
Always in production — redirect HTTP→HTTPS |
| Rate limiting |
Use bucket4j or gateway-level throttling |
| SQL injection |
Use parameterized queries / Spring Data — never string concatenation |
| XSS |
Validate/sanitize inputs; use Content-Security-Policy header |
Phase 7 — Testing (Weeks 31–34)
Testing pyramid for Spring Boot
/\
/E2E\ Few — full stack (Testcontainers + RestAssured)
/------\
/ Integ \ Some — slice tests (@WebMvcTest, @DataJpaTest)
/------------\
/ Unit \ Many — pure Java with Mockito
/----------------\
Unit test with Mockito
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private EmailService emailService;
@InjectMocks
private OrderService orderService;
@Test
void createOrder_shouldSaveAndSendEmail() {
// Arrange
CreateOrderRequest request = new CreateOrderRequest(1L, List.of());
Order savedOrder = new Order(1L, OrderStatus.PENDING);
when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);
// Act
OrderDto result = orderService.createOrder(request);
// Assert
assertThat(result.id()).isEqualTo(1L);
verify(emailService).sendOrderConfirmation(savedOrder);
verify(orderRepository).save(any(Order.class));
}
}
Integration test with @WebMvcTest
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void getUser_whenExists_returns200() throws Exception {
UserDto user = new UserDto(1L, "Alice", "alice@example.com");
when(userService.findById(1L)).thenReturn(user);
mockMvc.perform(get("/api/v1/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"))
.andExpect(jsonPath("$.email").value("alice@example.com"));
}
@Test
void createUser_withInvalidEmail_returns400() throws Exception {
CreateUserRequest request = new CreateUserRequest("Alice", "not-an-email", "pass123", 25);
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest());
}
}
Full integration test with Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class UserIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private TestRestTemplate restTemplate;
@Test
void createAndRetrieveUser() {
CreateUserRequest request = new CreateUserRequest("Bob", "bob@example.com", "password123", 30);
ResponseEntity<UserDto> created = restTemplate.postForEntity("/api/v1/users", request, UserDto.class);
assertThat(created.getStatusCode()).isEqualTo(HttpStatus.CREATED);
ResponseEntity<UserDto> retrieved = restTemplate.getForEntity(
"/api/v1/users/" + created.getBody().id(), UserDto.class);
assertThat(retrieved.getBody().email()).isEqualTo("bob@example.com");
}
}
Phase 8 — Messaging & async (Weeks 35–37)
Kafka with Spring Kafka
// Producer
@Service
public class OrderEventProducer {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public void publishOrderCreated(Order order) {
OrderEvent event = new OrderEvent(order.getId(), "ORDER_CREATED", Instant.now());
kafkaTemplate.send("order-events", order.getId().toString(), event);
}
}
// Consumer
@Component
public class NotificationConsumer {
@KafkaListener(topics = "order-events", groupId = "notification-service")
public void handleOrderEvent(OrderEvent event) {
if ("ORDER_CREATED".equals(event.type())) {
emailService.sendOrderConfirmation(event.orderId());
}
}
}
Async processing with @Async
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@Service
public class ReportService {
@Async("taskExecutor")
public CompletableFuture<Report> generateReport(Long userId) {
// Runs in thread pool, doesn't block HTTP thread
Report report = heavyComputation(userId);
return CompletableFuture.completedFuture(report);
}
}
Phase 9 — Microservices with Spring Cloud (Weeks 38–43)
Key Spring Cloud components
| Component |
Dependency |
What it does |
| Config Server |
spring-cloud-config-server |
Centralized config from Git |
| Eureka |
spring-cloud-starter-netflix-eureka-server |
Service discovery |
| Spring Cloud Gateway |
spring-cloud-starter-gateway |
API gateway + routing |
| OpenFeign |
spring-cloud-starter-openfeign |
Declarative HTTP client |
| Resilience4j |
spring-cloud-starter-circuitbreaker-resilience4j |
Circuit breaker, retry, rate limiter |
| Zipkin / Micrometer |
micrometer-tracing-bridge-brave |
Distributed tracing |
Feign client (service-to-service calls)
@FeignClient(name = "inventory-service", url = "${services.inventory.url}")
public interface InventoryClient {
@GetMapping("/api/v1/products/{productId}/stock")
StockResponse checkStock(@PathVariable Long productId);
}
// Usage in service
@Service
public class OrderService {
private final InventoryClient inventoryClient;
public Order createOrder(CreateOrderRequest request) {
StockResponse stock = inventoryClient.checkStock(request.productId());
if (stock.quantity() < request.quantity()) {
throw new InsufficientStockException("Not enough stock");
}
// ...
}
}
Circuit breaker with Resilience4j
@Service
public class PaymentService {
@CircuitBreaker(name = "payment-service", fallbackMethod = "paymentFallback")
@Retry(name = "payment-service")
@TimeLimiter(name = "payment-service")
public CompletableFuture<PaymentResult> processPayment(PaymentRequest request) {
return CompletableFuture.supplyAsync(() -> externalPaymentGateway.charge(request));
}
public CompletableFuture<PaymentResult> paymentFallback(PaymentRequest request, Exception ex) {
log.warn("Payment service unavailable, queuing for retry", ex);
pendingPaymentQueue.add(request);
return CompletableFuture.completedFuture(PaymentResult.queued(request.orderId()));
}
}
Phase 10 — DevOps (Weeks 44–46)
Multi-stage Dockerfile
# Build stage
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline -q
COPY src src
RUN ./mvnw package -DskipTests -q
# Run stage
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S spring && adduser -S spring -G spring
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
USER spring
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
docker-compose.yml for local development
version: '3.9'
services:
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: dev
DB_USER: myuser
DB_PASS: mypassword
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
GitHub Actions CI/CD
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: Run tests
run: ./mvnw verify
env:
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/testdb
SPRING_DATASOURCE_USERNAME: test
SPRING_DATASOURCE_PASSWORD: test
- name: Build Docker image
if: github.ref == 'refs/heads/main'
run: docker build -t myapp:${{ github.sha }} .
- name: Deploy to production
if: github.ref == 'refs/heads/main'
run: |
# Deploy to your cloud provider
echo "Deploying version ${{ github.sha }}"
Realistic timeline
| Month |
Focus |
Milestone |
| 1–2 |
Java fundamentals + OOP |
Build a console app using collections, streams |
| 3 |
Spring Core — DI, IoC, AOP |
Simple bean wiring exercise |
| 4 |
Spring Boot — first REST API |
CRUD API with H2 in-memory DB |
| 5–6 |
JPA, PostgreSQL, Flyway |
Full CRUD with persistent DB + migrations |
| 7 |
Spring Security + JWT |
Authenticated API with register/login |
| 8 |
Testing — unit + integration |
80%+ coverage on your API |
| 9 |
Kafka or RabbitMQ basics |
Event-driven order notification |
| 10–11 |
Docker, CI/CD, cloud deploy |
App live on Railway / Render / AWS |
| 12+ |
Spring Cloud, microservices |
Break monolith into 2–3 services |
Portfolio projects
| Project |
Skills demonstrated |
Complexity |
| Blog API |
CRUD, JPA, validation, pagination |
Beginner |
| Task manager + JWT auth |
Security, roles, protected endpoints |
Beginner |
| E-commerce API |
Orders, inventory, payments, JPA relations |
Intermediate |
| Real-time chat |
WebSocket, pub/sub, Redis |
Intermediate |
| Microservices app |
Spring Cloud, Feign, Config Server, Gateway |
Advanced |
| Event-driven system |
Kafka, async processing, CQRS pattern |
Advanced |
Spring Boot developer roles & salary
| Role |
Experience |
US salary |
EU salary |
| Junior Java/Spring Dev |
0–2 years |
$75k–$100k |
€40k–€60k |
| Mid-level Backend Dev |
2–5 years |
$100k–$140k |
€60k–€85k |
| Senior Java Engineer |
5+ years |
$140k–$180k |
€85k–€120k |
| Java Architect |
8+ years |
$160k–$220k |
€100k–€150k |
| Engineering Manager |
5+ years |
$160k–$230k |
€90k–€140k |
| Spring Boot Freelancer |
3+ years |
$80–$200/hr |
€60–€150/hr |
Common mistakes
| Mistake |
Why it's a problem |
Fix |
Using @Autowired on fields |
Harder to test, hides dependencies |
Constructor injection |
ddl-auto: create-drop in production |
Drops your schema on restart |
Use Flyway/Liquibase migrations |
| Not using DTOs |
Exposes internal model, security risk |
Map Entity → DTO at controller |
| Ignoring N+1 |
Kills database performance |
JOIN FETCH or @EntityGraph |
Catching Exception everywhere |
Swallows errors, hard to debug |
Handle specific exceptions |
| Hardcoding secrets |
Security vulnerability |
Use environment variables / Vault |
| No pagination on list endpoints |
Returns millions of rows |
Always use Pageable |
@Transactional on controller |
Wrong layer, can cause issues |
Only on service layer |
Spring Boot vs alternatives
| Framework |
Language |
Performance |
Learning curve |
Ecosystem |
Use case |
| Spring Boot |
Java |
Good |
Steep |
Massive |
Enterprise, microservices |
| Quarkus |
Java |
Excellent (native) |
Moderate |
Growing |
Cloud-native, GraalVM |
| Micronaut |
Java |
Excellent |
Moderate |
Good |
Low-memory microservices |
| NestJS |
TypeScript |
Good |
Gentle |
Large |
Node.js enterprise |
| FastAPI |
Python |
Good |
Gentle |
Large |
ML/AI backends |
| Go + Gin |
Go |
Excellent |
Moderate |
Good |
High-throughput APIs |
| Django REST |
Python |
Moderate |
Gentle |
Large |
Full-stack + APIs |
Frequently asked questions
How long does it take to learn Spring Boot?
With Java basics already known: 4–6 months to build real apps. From zero Java: 12–18 months to be job-ready. Spring Boot itself is learnable in weeks — the surrounding ecosystem (JPA, Security, Cloud) takes longer.
Do I need to learn Spring Framework before Spring Boot?
Not deeply — Spring Boot's auto-configuration hides most Spring Framework complexity. But understanding DI and IoC (Phase 2 of this roadmap) helps you debug when things go wrong.
Spring Boot vs Quarkus vs Micronaut?
Spring Boot wins on ecosystem size, job demand, and community. Quarkus/Micronaut are better for GraalVM native image (faster startup, less memory). For a first job: Spring Boot. For greenfield cloud-native: consider Quarkus.
Which Java version should I use?
Java 21 (LTS). Spring Boot 3.x requires Java 17+ and works best on 21. Avoid Java 8 for new projects.
Is Spring Boot good for microservices?
Yes — it's one of the most popular choices. Spring Cloud adds service discovery, config management, circuit breakers, and API gateway on top.
What database should I use with Spring Boot?
PostgreSQL for most new projects — it's powerful, open-source, and Spring Data JPA supports it perfectly. MySQL is a solid alternative. Use MongoDB when you genuinely need a document model.