An opinionated guide on developing web applications with Spring Boot.
MIT License
An opinionated guide on developing web applications with Spring Boot. Inspired by Airbnb JavaScript Style Guide.
constructor injection
. Avoid field injection
.Why? Constructor injection makes dependencies explicit and forces you to provide all mandatory dependencies when creating instances of your component.
// bad
public class PersonService {
@AutoWired
private PersonRepository personRepositoy;
}
// good
public class PersonService {
private final PersonRepository personRepository;
// if the class has only one constructor, @Autowired can be omitted
public PersonService(PersonRepository personRepository) {
this.personRepository = personRepository;
}
}
Why? A class already exposes an interface: its public members. Adding an identical
interface
definition makes the code harder to navigate and violates YAGNI.What about testing? Earlier mocking frameworks were only capable of mocking interfaces. Recent frameworks like Mockito can also mock classes.
// bad
public interface PersonService {
List<Person> getPersons();
}
public class PersonServiceImpl implements PersonService {
public List<Person> getPersons() {
// more code
}
}
// good
public class PersonService {
public List<Person> getPersons() {
// more code
}
}
@RestController
when providing a RESTful API.// bad
@Controller
public class PersonController {
@ResponseBody
@GetMapping("/persons/{id}")
public Person show(@PathVariable long id) {
// more code
}
}
// good
@RestController
public class PersonController {
@GetMapping("/persons/{id}")
public Person show(@PathVariable long id) {
// more code
}
}
@GetMapping
, @PostMapping
etc. instead of @RequestMapping
.// bad
@RestController
public class PersonController {
@RequestMapping(method = RequestMethod.GET, value = "/persons/{id}")
public Person show(@PathVariable long id) {
// more code
}
}
// good
@RestController
public class PersonController {
@GetMapping("/persons/{id}")
public Person show(@PathVariable long id) {
// more code
}
}
Why? To avoid SRP violations;
Where should I put my business logic? Keep the bussines logic encapsulated into your services or specialized classes;
// bad
@PostMapping("/users")
public ResponseEntity<Void> postNewUser(@RequestBody UserRequest userRequest) {
if (userRequest.isLessThanEighteenYearsOld()) {
throw new IllegalArgumentException("Sorry, only users greater or equal than 18 years old.");
}
if (!userRequest.hasJob()) {
throw new IllegalArgumentException("Sorry, only users working.");
}
if (!this.userService.hasUsernameAvailable(userRequest.getUsername())) {
throw new IllegalArgumentException(String.format("Sorry, [%s] is not an available username.", userRequest.getUsername()));
}
this.userService.createNewUser(userRequest);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
// good
@PostMapping("/users")
public ResponseEntity<Void> postNewUser(@RequestBody UserRequest userRequest) {
this.userService.createNewUser(userRequest);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
public class UserService {
// variables declaration
public void createNewUser(UserRequest userRequest) {
this.validateNewUser(userRequest);
UserEntity newUserEntity = this.userMapper.mapToEntity(userRequest);
this.userRepository.save(newUserEntity);
}
private void validateNewUser(UserRequest userRequest) {
// business validations
}
}
JavaBeans
.Why? JavaBeans are mutable and split object construction across multiple calls.
// bad
public class Person {
private String firstname;
private String lastname;
public void setFirstname() {
this.firstname = firstname;
}
public String getFirstname() {
return firstname;
}
public void setLastname() {
this.lastname = lastname;
}
public String getLastname() {
return lastname;
}
}
// good
public class Person {
private final String firstname;
private final String lastname;
// requires your code to be compiled with a Java 8 compliant compiler
// with the -parameter flag turned on
// as of Spring Boot 2.0 or higher, this is the default
@JsonCreator
public Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public String getFirstname() {
return firstname;
}
public String getLastname() {
return lastname;
}
}
// best
public class Person {
private final String firstname;
private final String lastname;
// if the class has a only one constructor, @JsonCreator can be omitted
public Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public String getFirstname() {
return firstname;
}
public String getLastname() {
return lastname;
}
}
class PersonServiceTests {
@Test
void testGetPersons() {
// given
PersonRepository personRepository = mock(PersonRepository.class);
when(personRepository.findAll()).thenReturn(List.of(new Person("Oliver", "Weiler")));
PersonService personService = new PersonService(personRepository);
// when
List<Person> persons = personService.getPersons();
// then
assertThat(persons).extracting(Person::getFirstname, Person::getLastname).containsExactly("Oliver", "Weiler");
}
}
Why?
AssertJ
is more actively developed, requires only one static import, and allows you to discover assertions through autocompletion.
// bad
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.empty;
assertThat(persons), is(not(empty())));
// good
import static org.assertj.core.api.Assertions.assertThat;
assertThat(persons).isNotEmpty();