Skip to content
Spring Framework Guide — Enterprise Java Development

Spring Framework Guide — Enterprise Java Development

DodaTech Updated Jun 7, 2026 9 min read

Spring Framework is the most widely used enterprise Java framework, providing comprehensive infrastructure support for building robust, scalable applications through dependency injection, aspect-oriented programming, and a vast ecosystem of sub-projects for every layer of application architecture.

What You’ll Learn

You’ll understand Inversion of Control and dependency injection, create Spring Boot applications with auto-configuration, build REST APIs with Spring MVC, access databases with Spring Data JPA, secure endpoints with Spring Security, and configure everything with application.properties.

Why Spring Matters

Spring dominates enterprise Java development. It’s used by banks, e-commerce platforms, and SaaS providers worldwide because it provides a cohesive, testable, and production-ready architecture. DodaTech’s backend services use Spring Boot for REST APIs that serve data to Doda Browser — its mature security features (CSRF protection, authentication providers, method-level security) help us maintain PCI DSS compliance.

Spring Learning Path

    flowchart LR
  A[Java & Maven Basics] --> B[Spring Framework]
  B --> C[IoC & DI]
  C --> D[Spring Boot]
  D --> E[Spring MVC]
  D --> F[Spring Data JPA]
  D --> G[Spring Security]
  E --> H[REST APIs]
  B:::current

  classDef current fill:#6DB33F,color:#fff,stroke:#333,stroke-width:2px
  
Prerequisites: Solid Java fundamentals (classes, interfaces, annotations, collections). Familiarity with Maven or Gradle for build management is expected.

Inversion of Control and Dependency Injection

IoC is Spring’s core concept. Instead of objects creating their dependencies, Spring creates and injects them:

// Without DI — tightly coupled, hard to test
public class EmailService {
    private SmtpServer server = new SmtpServer();
    
    public void send(String to, String body) {
        server.send(to, body);
    }
}

// With DI — loosely coupled, testable
public class EmailService {
    private final MailServer server;
    
    // Dependency is injected, not created
    public EmailService(MailServer server) {
        this.server = server;
    }
    
    public void send(String to, String body) {
        server.send(to, body);
    }
}

Spring manages this through its IoC container, which creates beans (managed objects) and wires them together:

@Component  // Marks this as a Spring-managed bean
public class SmtpMailServer implements MailServer {
    // ...
}

@Component
public class EmailService {
    private final MailServer server;
    
    @Autowired  // Spring injects SmtpMailServer automatically
    public EmailService(MailServer server) {
        this.server = server;
    }
}

Output: Spring scans the classpath for @Component classes, creates instances, and injects them where @Autowired is declared. No new keywords needed. Testing becomes easy — pass a mock MailServer constructor.

Spring Boot — Auto-Configuration

Spring Boot removes boilerplate configuration by auto-configuring beans based on classpath dependencies:

// Application.java — Entry point
@SpringBootApplication  // Combines @Configuration, @EnableAutoConfiguration, @ComponentScan
public class DodaTechApplication {
    public static void main(String[] args) {
        SpringApplication.run(DodaTechApplication.class, args);
    }
}
# application.properties — Configuration
server.port=8080
spring.application.name=dodatech-api

# DataSource configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/dodatech
spring.datasource.username=app_user
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true

Output: Running the application starts an embedded Tomcat server on port 8080, connects to PostgreSQL, and configures Hibernate. Adding spring-boot-starter-web to dependencies automatically configures Spring MVC with Jackson for JSON serialization — no XML configuration needed.

Spring MVC — Building REST APIs

Spring MVC uses annotations to map HTTP requests to handler methods:

@RestController
@RequestMapping("/api/v1/devices")
public class DeviceController {
    
    private final DeviceService deviceService;
    
    public DeviceController(DeviceService deviceService) {
        this.deviceService = deviceService;
    }
    
    @GetMapping
    public ResponseEntity<List<DeviceDTO>> getAllDevices(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        List<DeviceDTO> devices = deviceService.findAll(page, size);
        return ResponseEntity.ok(devices);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<DeviceDTO> getDevice(@PathVariable Long id) {
        return deviceService.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
    
    @PostMapping
    public ResponseEntity<DeviceDTO> createDevice(@Valid @RequestBody CreateDeviceRequest request) {
        DeviceDTO created = deviceService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<DeviceDTO> updateDevice(
            @PathVariable Long id,
            @Valid @RequestBody UpdateDeviceRequest request) {
        DeviceDTO updated = deviceService.update(id, request);
        return ResponseEntity.ok(updated);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteDevice(@PathVariable Long id) {
        deviceService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

Output: GET /api/v1/devices returns a paginated list of devices as JSON. POST /api/v1/devices creates a new device and returns 201 Created. Input validation is handled by @Valid and jakarta.validation annotations on the request DTO.

Spring Data JPA

Spring Data JPA eliminates the need for DAO implementations by generating queries automatically:

// Entity
@Entity
@Table(name = "devices")
public class Device {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String name;
    
    @Column(nullable = false)
    private String type;
    
    @Column(name = "signal_strength")
    private Integer signalStrength;
    
    @Column(nullable = false)
    private Boolean connected = false;
    
    @CreatedDate
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
    
    // Getters and setters...
}
// Repository — Spring Data JPA generates the implementation
public interface DeviceRepository extends JpaRepository<Device, Long> {
    
    // Query methods derived from method names
    List<Device> findByType(String type);
    
    List<Device> findByConnectedTrue();
    
    Page<Device> findBySignalStrengthGreaterThan(int minSignal, Pageable pageable);
    
    @Query("SELECT d FROM Device d WHERE d.name LIKE %:keyword% OR d.type LIKE %:keyword%")
    Page<Device> search(@Param("keyword") String keyword, Pageable pageable);
    
    long countByConnectedTrue();
}
// Service layer
@Service
@Transactional
public class DeviceService {
    
    private final DeviceRepository repository;
    private final DeviceMapper mapper;
    
    public DeviceService(DeviceRepository repository, DeviceMapper mapper) {
        this.repository = repository;
        this.mapper = mapper;
    }
    
    public List<DeviceDTO> findAll(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("name"));
        return repository.findAll(pageable)
                .map(mapper::toDto)
                .getContent();
    }
    
    public Optional<DeviceDTO> findById(Long id) {
        return repository.findById(id).map(mapper::toDto);
    }
    
    public DeviceDTO create(CreateDeviceRequest request) {
        if (repository.findByName(request.name()).isPresent()) {
            throw new DuplicateResourceException("Device name already exists");
        }
        
        Device device = mapper.toEntity(request);
        Device saved = repository.save(device);
        return mapper.toDto(saved);
    }
    
    public DeviceDTO update(Long id, UpdateDeviceRequest request) {
        Device device = repository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Device not found: " + id));
        
        mapper.updateEntity(request, device);
        Device saved = repository.save(device);
        return mapper.toDto(saved);
    }
    
    public void delete(Long id) {
        if (!repository.existsById(id)) {
            throw new ResourceNotFoundException("Device not found: " + id);
        }
        repository.deleteById(id);
    }
}

Output: findByType("IoT") generates and executes SELECT * FROM devices WHERE type = ? automatically. search("sensor", pageable) runs the custom @Query. The service layer adds transactional behavior, validation, and exception handling on top of the repository.

Spring Security

Protect your API endpoints with authentication and authorization:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())  // REST APIs don't need CSRF
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.GET, "/api/v1/devices/**").authenticated()
                .requestMatchers(HttpMethod.POST, "/api/v1/devices/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.DELETE, "/api/v1/devices/**").hasRole("ADMIN")
                .requestMatchers("/actuator/**").hasRole("ADMIN")
                .requestMatchers("/api/v1/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
// Method-level security
@RestController
@RequestMapping("/api/v1/admin")
public class AdminController {
    
    @GetMapping("/dashboard")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<DashboardDTO> getDashboard() {
        // Only accessible to ADMIN role
        return ResponseEntity.ok(dashboardService.getSummary());
    }
    
    @PostMapping("/users/{userId}/suspend")
    @PreAuthorize("hasAuthority('USER_SUSPEND')")
    public ResponseEntity<Void> suspendUser(@PathVariable Long userId) {
        userService.suspend(userId);
        return ResponseEntity.noContent().build();
    }
}

Output: Unauthenticated requests return 401. GET requests with a valid JWT proceed to the controller. POST/DELETE requests without ADMIN role return 403. Method-level annotations provide fine-grained access control.

Security Angle: Input Validation and Sanitization

// Request DTO with validation annotations
public record CreateDeviceRequest(
    @NotBlank(message = "Device name is required")
    @Size(min = 2, max = 100, message = "Name must be 2-100 characters")
    @Pattern(regexp = "^[a-zA-Z0-9-_]+$", message = "Invalid characters in name")
    String name,
    
    @NotBlank(message = "Device type is required")
    String type,
    
    @Min(value = -100, message = "Signal strength too low")
    @Max(value = 0, message = "Signal strength too high")
    Integer signalStrength,
    
    @Email(message = "Invalid email format")
    String contactEmail
) {}
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .toList();
        
        return ResponseEntity
            .badRequest()
            .body(new ErrorResponse("Validation failed", errors));
    }
    
    @ExceptionHandler(DataIntegrityViolationException.class)
    public ResponseEntity<ErrorResponse> handleDuplicateKey() {
        return ResponseEntity
            .status(HttpStatus.CONFLICT)
            .body(new ErrorResponse("Resource already exists"));
    }
}

DodaTech uses this same pattern across all Spring Boot services to prevent SQL injection, XSS, and data corruption before they reach the database layer.

Common Mistakes Beginners Make

  1. Field injection with @Autowired: Field injection (@Autowired private Service s) makes testing difficult and breaks encapsulation. Use constructor injection instead.

  2. Not using @Transactional properly: Database operations outside transactions fail with LazyInitializationException. Mark service methods with @Transactional and keep transactions short.

  3. Ignoring connection pooling: Spring Boot uses HikariCP by default, but default pool size (10) may be too small. Configure spring.datasource.hikari.maximum-pool-size based on load testing.

  4. Overusing @RequestMapping: Use specific annotations (@GetMapping, @PostMapping) for better readability and Swagger documentation generation.

  5. Not handling idempotency for PUT requests: PUT should be idempotent. Ensure calling the same PUT request multiple times produces the same result without side effects.

  6. Exposing entities directly in REST responses: Never return JPA entities as JSON responses. Use DTOs to decouple the API contract from the database schema and avoid lazy loading issues.

Practice Questions

  1. What is the difference between @Component, @Service, and @Repository?
  2. How does constructor injection differ from field injection?
  3. What does @SpringBootApplication include?
  4. How do you customize Spring Data JPA queries?
  5. What is the purpose of @Transactional?

Answers:

  1. All three register beans in the IoC container. @Service and @Repository are specializations of @Component that add semantic meaning and exception translation (for repositories).
  2. Constructor injection makes dependencies explicit, enables immutable fields (final), simplifies testing (no reflection), and prevents null states. Field injection hides dependencies and requires mocking frameworks.
  3. @SpringBootApplication combines @Configuration (bean definitions), @EnableAutoConfiguration (auto-configuration), and @ComponentScan (component scanning).
  4. Use derived query methods (method name parsing), @Query annotations with JPQL or native SQL, or Specification/QueryDSL for dynamic queries.
  5. @Transactional demarcates a transaction boundary, ensuring all database operations within the method execute in a single transaction that commits or rolls back atomically.

Challenge

Build a complete REST API for a device inventory system: create JPA entities with relationships, implement CRUD endpoints with pagination, sorting, and filtering, add Spring Security with JWT authentication, write integration tests with TestContainers, and document the API with OpenAPI/Swagger.

Real-World Task

Create a URL shortening service with Spring Boot: generate unique short codes, redirect to original URLs (301/302), track click analytics, implement rate limiting with Bucket4j, cache popular URLs with Redis through Spring Cache, and expose management endpoints via Spring Boot Actuator.

FAQ

What is the difference between Spring Framework and Spring Boot?
: Spring Framework provides the core IoC container and infrastructure. Spring Boot adds auto-configuration, embedded servers, production-ready features (metrics, health checks), and streamlined dependency management through starters.
Does Spring require a Java application server?
: No. Spring Boot includes embedded Tomcat, Jetty, or Undertow. You run java -jar app.jar without any external server installation.
How does Spring handle database transactions?
: Through @Transactional with declarative transaction management backed by Spring AOP. It supports programmatic transaction management via TransactionTemplate for fine-grained control.
Can I use Spring with Kotlin?
: Yes. Spring has first-class Kotlin support including Kotlin-specific DSLs for bean definitions, routing, and mocking.
Is Spring suitable for microservices?
: Yes. Spring Boot is the most popular Java framework for microservices, and Spring Cloud provides service discovery, configuration management, circuit breakers, and API gateways.

Try It Yourself

# Create a Spring Boot project with Spring Initializr
curl https://start.spring.io/starter.zip \
  -d dependencies=web,data-jpa,postgresql,validation,security \
  -d name=dodatech-api \
  -d packageName=com.dodatech.api \
  -o dodatech-api.zip

unzip dodatech-api.zip
cd dodatech-api
./mvnw spring-boot:run

Create a simple REST controller with @RestController, add a @GetMapping endpoint, and test it at http://localhost:8080/api/hello.

What’s Next

Related topics: Java, Maven, Spring Boot, JPA

What’s Next

Congratulations on completing this Spring Framework tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro