Spring Framework Guide — Enterprise Java Development
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
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=trueOutput: 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
Field injection with
@Autowired: Field injection (@Autowired private Service s) makes testing difficult and breaks encapsulation. Use constructor injection instead.Not using
@Transactionalproperly: Database operations outside transactions fail with LazyInitializationException. Mark service methods with@Transactionaland keep transactions short.Ignoring connection pooling: Spring Boot uses HikariCP by default, but default pool size (10) may be too small. Configure
spring.datasource.hikari.maximum-pool-sizebased on load testing.Overusing
@RequestMapping: Use specific annotations (@GetMapping,@PostMapping) for better readability and Swagger documentation generation.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.
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
- What is the difference between
@Component,@Service, and@Repository? - How does constructor injection differ from field injection?
- What does
@SpringBootApplicationinclude? - How do you customize Spring Data JPA queries?
- What is the purpose of
@Transactional?
Answers:
- All three register beans in the IoC container.
@Serviceand@Repositoryare specializations of@Componentthat add semantic meaning and exception translation (for repositories). - 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. @SpringBootApplicationcombines@Configuration(bean definitions),@EnableAutoConfiguration(auto-configuration), and@ComponentScan(component scanning).- Use derived query methods (method name parsing),
@Queryannotations with JPQL or native SQL, orSpecification/QueryDSLfor dynamic queries. @Transactionaldemarcates 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
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:runCreate 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