Back to blog
Backend Systemsbeginner

Spring Boot: Setup, Auto-Configuration & Project Structure

Bootstrap a production Spring Boot application — understand auto-configuration, the application context, profiles, configuration properties, dependency injection, and structure a real-world multi-layer project.

LearnixoApril 16, 20265 min read
Spring BootJavaBackendSpring FrameworkDependency InjectionIoC
Share:𝕏

What Is Spring Boot?

Spring Boot is an opinionated framework that makes it trivial to build production-ready Spring applications. It eliminates XML configuration and manual bean wiring by auto-configuring the application based on what's on the classpath.

Spring Framework (IoC container, AOP, data, security)
    ↑
Spring Boot (auto-config, embedded server, starter dependencies)
    ↑
Your application code

Bootstrapping with Spring Initializr

The fastest way to start — use start.spring.io:

  • Project: Maven or Gradle
  • Language: Java
  • Spring Boot: 3.x (requires Java 17+)
  • Dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver, Spring Security, Validation, Actuator

Or with the CLI:

Bash
curl https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.4.0 \
  -d groupId=com.clinic \
  -d artifactId=portal-api \
  -d name=PortalApi \
  -d javaVersion=21 \
  -d dependencies=web,data-jpa,postgresql,security,validation,actuator \
  -o portal-api.zip && unzip portal-api.zip

Project Structure

A well-structured Spring Boot project follows package-by-feature, not package-by-layer:

src/main/java/com/clinic/portal/
├── PortalApiApplication.java       ← entry point
├── appointment/
│   ├── Appointment.java            ← JPA entity
│   ├── AppointmentRepository.java  ← Spring Data repo
│   ├── AppointmentService.java     ← business logic
│   ├── AppointmentController.java  ← REST controller
│   └── dto/
│       ├── AppointmentRequest.java
│       └── AppointmentResponse.java
├── patient/
│   ├── Patient.java
│   ├── PatientRepository.java
│   ├── PatientService.java
│   └── PatientController.java
├── security/
│   ├── SecurityConfig.java
│   ├── JwtAuthFilter.java
│   └── JwtService.java
└── common/
    ├── exception/
    │   └── GlobalExceptionHandler.java
    └── config/
        └── OpenApiConfig.java

The Application Context & Dependency Injection

Spring's core: an IoC container that manages the lifecycle of your objects (called beans).

Declaring Beans

JAVA
// @Component — generic bean
@Component
public class EmailService { ... }

// @Service — business logic layer (semantic alias for @Component)
@Service
public class AppointmentService { ... }

// @Repository — data access layer (adds exception translation)
@Repository
public class AppointmentRepository { ... }

// @Controller / @RestController — web layer
@RestController
public class AppointmentController { ... }

// @Configuration + @Bean — manual bean declaration
@Configuration
public class AppConfig {
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .registerModule(new JavaTimeModule());
    }
}

Constructor Injection (preferred)

JAVA
@Service
public class AppointmentService {

    private final AppointmentRepository repository;
    private final PatientService patientService;
    private final EmailService emailService;

    // Spring injects dependencies through the constructor
    // No @Autowired needed when there's only one constructor (Spring 4.3+)
    public AppointmentService(AppointmentRepository repository,
                               PatientService patientService,
                               EmailService emailService) {
        this.repository     = repository;
        this.patientService = patientService;
        this.emailService   = emailService;
    }
}

Why constructor injection?

  • final fields — immutable after construction
  • Easy to test — just pass mocks to the constructor
  • Makes dependencies explicit

Configuration & Profiles

application.yml

YAML
# src/main/resources/application.yml

spring:
  application:
    name: portal-api

  datasource:
    url: jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:portal_dev}
    username: ${DB_USER:postgres}
    password: ${DB_PASSWORD:postgres}
    hikari:
      maximum-pool-size: 20
      connection-timeout: 30000

  jpa:
    hibernate:
      ddl-auto: validate       # never "create-drop" in prod
    show-sql: false
    properties:
      hibernate.format_sql: true

  security:
    jwt:
      secret: ${JWT_SECRET}
      expiration-ms: 3600000   # 1 hour

server:
  port: 8080
  shutdown: graceful
  error:
    include-message: never     # don't expose internals

management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics, prometheus

Profiles

Different configs for dev, test, production:

YAML
# application-dev.yml
spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
  datasource:
    url: jdbc:postgresql://localhost:5432/portal_dev

logging:
  level:
    com.clinic: DEBUG
YAML
# application-prod.yml
spring:
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate

Activate profiles:

Bash
# Environment variable (recommended in production)
SPRING_PROFILES_ACTIVE=prod java -jar portal-api.jar

# Or as JVM arg
java -Dspring.profiles.active=prod -jar portal-api.jar

@ConfigurationProperties — Type-Safe Config

JAVA
@ConfigurationProperties(prefix = "spring.security.jwt")
public record JwtProperties(String secret, long expirationMs) {}

// Register it
@SpringBootApplication
@EnableConfigurationProperties(JwtProperties.class)
public class PortalApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(PortalApiApplication.class, args);
    }
}

// Inject and use
@Service
public class JwtService {
    private final JwtProperties jwtProps;

    public JwtService(JwtProperties jwtProps) {
        this.jwtProps = jwtProps;
    }

    public String generateToken(String subject) {
        return Jwts.builder()
            .subject(subject)
            .expiration(new Date(System.currentTimeMillis() + jwtProps.expirationMs()))
            .signWith(Keys.hmacShaKeyFor(jwtProps.secret().getBytes()))
            .compact();
    }
}

Auto-Configuration Explained

When you add spring-boot-starter-data-jpa to your classpath, Spring Boot automatically:

  1. Detects javax.persistence.Entity on your classpath
  2. Configures a DataSource from spring.datasource.* properties
  3. Creates a JPA EntityManagerFactory
  4. Creates a PlatformTransactionManager
  5. Enables Spring Data repository scanning

You can see what was configured:

Bash
java -jar portal-api.jar --debug 2>&1 | grep "AUTO-CONFIGURATION REPORT"

Or set logging.level.org.springframework.boot.autoconfigure=DEBUG.

To exclude an auto-configuration:

JAVA
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })

Bean Scopes

| Scope | Lifetime | Default? | |-------|----------|---------| | singleton | One instance per application context | Yes | | prototype | New instance every injection | No | | request | One per HTTP request (web only) | No | | session | One per HTTP session (web only) | No |

99% of your beans should be singleton. Use prototype only for stateful beans that can't be shared.


ApplicationRunner — Startup Logic

JAVA
@Component
public class StartupRunner implements ApplicationRunner {

    private final AppointmentRepository repo;

    public StartupRunner(AppointmentRepository repo) {
        this.repo = repo;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        long count = repo.count();
        log.info("Application started. {} appointments in database.", count);
    }
}

Key Annotations Reference

| Annotation | Purpose | |-----------|---------| | @SpringBootApplication | @Configuration + @EnableAutoConfiguration + @ComponentScan | | @Component | Generic bean | | @Service | Business logic bean | | @Repository | Data access bean | | @RestController | Controller bean that writes JSON to the response body | | @RequestMapping | Map URL path to a controller or method | | @Autowired | Inject a bean (prefer constructor injection) | | @Value("${prop}") | Inject a single config value | | @ConfigurationProperties | Bind a config prefix to a typed object | | @Profile("dev") | Only create this bean in the "dev" profile | | @Conditional | Conditionally create a bean based on any logic |

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.