Spring Boot makes building APIs feel deceptively easy.
When I started out, I focused on just “getting things working.” But when things break—or just scale badly—you realize that working code isn’t always good code.
Here are 7 mistakes I made writing my first Spring Boot APIs, and what I now do to write cleaner, more maintainable backends.
1. Not Using Validation Annotations (@Valid
, @NotNull
, etc.)
What I did:
Manually validated request payloads deep inside controllers or services. Missed fields caused confusing errors or null pointer exceptions.
if (userRequest.getEmail() == null) {
throw new IllegalArgumentException("Email is required");
}
Why it's a problem:
- Makes code noisy and repetitive
- Delays validation too deep into the flow
- Prone to human error
What I do now:
Use Spring's built-in validation support with annotations and @Valid
.
public class UserRequest {
@NotNull
@Email
private String email;
}
And in the controller:
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody UserRequest request) {
...
}
Pair this with @ControllerAdvice
to handle validation exceptions cleanly.
2. Hardcoding config with @Value
instead of using @ConfigurationProperties
What I did:
Injected config values everywhere using @Value
.
@Value("${aws.s3.bucket}")
private String bucketName;
Why it's a problem:
- Scattered config makes things hard to manage
- No way to validate values
- Harder to write unit tests
What I do now:
Use @ConfigurationProperties
to bind entire config blocks into structured classes.
@ConfigurationProperties(prefix = "aws.s3")
public class S3Properties {
private String bucket;
// getters/setters
}
This gives you validation, IDE support, and cleaner wiring.
3. Logging the Wrong Way
What I did:
Used System.out.println()
and vague messages like “Error occurred” or “Debug here”.
Why it's a problem:
- Not visible in production logs
- Lacks context (who, what, when)
- No severity levels or structured format
What I do now:
Use SLF4J with clear, parameterized logging:
log.info("User {} uploaded file {}", userId, fileName);
And for errors:
log.error("Failed to process request for user {}", userId, e);
Be intentional with log levels: info
, warn
, error
, and avoid logging entire stack traces unless necessary.
4. Messy Exception Handling
What I did:
Wrapped everything in try-catch blocks, often rethrowing or printing stack traces manually.
try {
...
} catch (Exception e) {
e.printStackTrace();
throw e;
}
Why it's a problem:
- Duplicates logic
- Makes error responses inconsistent
- Hides real issues in production
What I do now:
Define custom exceptions and handle them centrally using @ControllerAdvice
.
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String msg) {
super(msg);
}
}
And in a global exception handler:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
}
Clean, consistent, and maintainable.
5. Overusing @Autowired
Field Injection
What I did:
Injected everything using @Autowired
on fields.
@Autowired
private UserService userService;
Why it's a problem:
- Makes dependencies hard to track
- Breaks immutability
- Difficult to write unit tests
What I do now:
Use constructor injection:
@Service
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
Bonus: With one constructor, Spring injects it automatically—no @Autowired
needed.
6. Forgetting API Documentation (like Swagger/OpenAPI)
What I did:
Left API documentation in someone's Notion page or none at all.
Why it's a problem:
- Frontend teams have to guess request/response formats
- No way to test endpoints quickly
- Becomes a bottleneck during integration
What I do now:
Use springdoc-openapi to generate live docs with minimal setup:
<!-- build.gradle / pom.xml -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.14</version>
</dependency>
Then access Swagger UI at:
http://localhost:8080/swagger-ui/index.html
It reflects your controller annotations and helps both devs and testers.
7. Mixing DTOs with Entities
What I did:
Used JPA entities directly in request bodies and responses.
Why it's a problem:
- Leaks internal persistence logic
- Adds accidental complexity (lazy loading, cascade issues)
- Makes refactoring risky
What I do now:
Create dedicated DTO classes:
public class UserDto {
private String name;
private String email;
}
Use mappers like MapStruct, ModelMapper, or just write a utility:
public UserDto toDto(User user) {
return new UserDto(user.getName(), user.getEmail());
}
Entities stay clean. APIs stay stable.
đź’¬ Final Thoughts
Spring Boot abstracts a lot of complexity—but with that power comes responsibility.
These 7 mistakes cost me time, performance, and headaches. But fixing them made me a better engineer.
What’s one mistake you made in your early Spring Boot journey?
Would love to hear your thoughts below 👇
Top comments (0)