├── LICENSE └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Oliver Weiler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Style Guide 2 | 3 | An opinionated guide on developing web applications with Spring Boot. Inspired by [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). 4 | 5 | [![Join the chat at https://gitter.im/helpermethod/spring-boot-style-guide](https://badges.gitter.im/helpermethod/spring-boot-style-guide.svg)](https://gitter.im/helpermethod/spring-boot-style-guide?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/helpermethod/spring-boot-style-guide/master/LICENSE) 7 | 8 | ## Table of Contents 9 | 10 | * [Dependency Injection](#dependency-injection) 11 | * [Controllers](#controllers) 12 | * [Serialization](#serialization) 13 | * [Testing](#testing) 14 | 15 | ## Dependency Injection 16 | 17 | * Use `constructor injection`. Avoid `field injection`. 18 | 19 | > Why? Constructor injection makes dependencies explicit and forces you to provide all mandatory dependencies when creating instances of your component. 20 | 21 | ```java 22 | // bad 23 | public class PersonService { 24 | @AutoWired 25 | private PersonRepository personRepositoy; 26 | } 27 | 28 | // good 29 | public class PersonService { 30 | private final PersonRepository personRepository; 31 | 32 | // if the class has only one constructor, @Autowired can be omitted 33 | public PersonService(PersonRepository personRepository) { 34 | this.personRepository = personRepository; 35 | } 36 | } 37 | ``` 38 | 39 | * Avoid single implementation interfaces. 40 | 41 | > Why? A class already exposes an interface: its public members. Adding an identical `interface` definition makes the code harder to navigate and violates [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it). 42 | > 43 | > What about testing? Earlier mocking frameworks were only capable of mocking interfaces. Recent frameworks like [Mockito](https://site.mockito.org/) can also mock classes. 44 | 45 | ```java 46 | // bad 47 | public interface PersonService { 48 | List getPersons(); 49 | } 50 | 51 | public class PersonServiceImpl implements PersonService { 52 | public List getPersons() { 53 | // more code 54 | } 55 | } 56 | 57 | // good 58 | public class PersonService { 59 | public List getPersons() { 60 | // more code 61 | } 62 | } 63 | ``` 64 | 65 | **[⬆ back to top](#table-of-contents)** 66 | 67 | ## Controllers 68 | 69 | * Use `@RestController` when providing a RESTful API. 70 | 71 | ```java 72 | // bad 73 | @Controller 74 | public class PersonController { 75 | @ResponseBody 76 | @GetMapping("/persons/{id}") 77 | public Person show(@PathVariable long id) { 78 | // more code 79 | } 80 | } 81 | 82 | // good 83 | @RestController 84 | public class PersonController { 85 | @GetMapping("/persons/{id}") 86 | public Person show(@PathVariable long id) { 87 | // more code 88 | } 89 | } 90 | ``` 91 | 92 | * Use `@GetMapping`, `@PostMapping` etc. instead of `@RequestMapping`. 93 | 94 | ```java 95 | // bad 96 | @RestController 97 | public class PersonController { 98 | @RequestMapping(method = RequestMethod.GET, value = "/persons/{id}") 99 | public Person show(@PathVariable long id) { 100 | // more code 101 | } 102 | } 103 | 104 | // good 105 | @RestController 106 | public class PersonController { 107 | @GetMapping("/persons/{id}") 108 | public Person show(@PathVariable long id) { 109 | // more code 110 | } 111 | } 112 | ``` 113 | 114 | * Simplify your controller keeping it thin 115 | 116 | > Why? To avoid [SRP](https://en.wikipedia.org/wiki/Single-responsibility_principle#:~:text=The%20single%2Dresponsibility%20principle%20(SRP,it%20should%20encapsulate%20that%20part.)) violations; 117 | 118 | > Where should I put my business logic? Keep the bussines logic [encapsulated](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)) into your services or specialized classes; 119 | 120 | ```java 121 | // bad 122 | @PostMapping("/users") 123 | public ResponseEntity postNewUser(@RequestBody UserRequest userRequest) { 124 | if (userRequest.isLessThanEighteenYearsOld()) { 125 | throw new IllegalArgumentException("Sorry, only users greater or equal than 18 years old."); 126 | } 127 | 128 | if (!userRequest.hasJob()) { 129 | throw new IllegalArgumentException("Sorry, only users working."); 130 | } 131 | 132 | if (!this.userService.hasUsernameAvailable(userRequest.getUsername())) { 133 | throw new IllegalArgumentException(String.format("Sorry, [%s] is not an available username.", userRequest.getUsername())); 134 | } 135 | 136 | this.userService.createNewUser(userRequest); 137 | 138 | return ResponseEntity.status(HttpStatus.CREATED).build(); 139 | } 140 | 141 | // good 142 | @PostMapping("/users") 143 | public ResponseEntity postNewUser(@RequestBody UserRequest userRequest) { 144 | this.userService.createNewUser(userRequest); 145 | return ResponseEntity.status(HttpStatus.CREATED).build(); 146 | } 147 | 148 | public class UserService { 149 | 150 | // variables declaration 151 | 152 | public void createNewUser(UserRequest userRequest) { 153 | this.validateNewUser(userRequest); 154 | UserEntity newUserEntity = this.userMapper.mapToEntity(userRequest); 155 | this.userRepository.save(newUserEntity); 156 | } 157 | 158 | private void validateNewUser(UserRequest userRequest) { 159 | // business validations 160 | } 161 | } 162 | ``` 163 | 164 | **[⬆ back to top](#table-of-contents)** 165 | 166 | ## Serialization 167 | 168 | * Do not map your JSON objects to `JavaBeans`. 169 | 170 | > Why? JavaBeans are mutable and split object construction across multiple calls. 171 | 172 | ```java 173 | // bad 174 | public class Person { 175 | private String firstname; 176 | private String lastname; 177 | 178 | public void setFirstname() { 179 | this.firstname = firstname; 180 | } 181 | 182 | public String getFirstname() { 183 | return firstname; 184 | } 185 | 186 | public void setLastname() { 187 | this.lastname = lastname; 188 | } 189 | 190 | public String getLastname() { 191 | return lastname; 192 | } 193 | } 194 | 195 | // good 196 | public class Person { 197 | private final String firstname; 198 | private final String lastname; 199 | 200 | // requires your code to be compiled with a Java 8 compliant compiler 201 | // with the -parameter flag turned on 202 | // as of Spring Boot 2.0 or higher, this is the default 203 | @JsonCreator 204 | public Person(String firstname, String lastname) { 205 | this.firstname = firstname; 206 | this.lastname = lastname; 207 | } 208 | 209 | public String getFirstname() { 210 | return firstname; 211 | } 212 | 213 | public String getLastname() { 214 | return lastname; 215 | } 216 | } 217 | 218 | // best 219 | public class Person { 220 | private final String firstname; 221 | private final String lastname; 222 | 223 | // if the class has a only one constructor, @JsonCreator can be omitted 224 | public Person(String firstname, String lastname) { 225 | this.firstname = firstname; 226 | this.lastname = lastname; 227 | } 228 | 229 | public String getFirstname() { 230 | return firstname; 231 | } 232 | 233 | public String getLastname() { 234 | return lastname; 235 | } 236 | } 237 | ``` 238 | 239 | **[⬆ back to top](#table-of-contents)** 240 | 241 | ## Testing 242 | 243 | * Keep Spring out of your unit tests. 244 | 245 | ```java 246 | class PersonServiceTests { 247 | @Test 248 | void testGetPersons() { 249 | // given 250 | PersonRepository personRepository = mock(PersonRepository.class); 251 | when(personRepository.findAll()).thenReturn(List.of(new Person("Oliver", "Weiler"))); 252 | 253 | PersonService personService = new PersonService(personRepository); 254 | 255 | // when 256 | List persons = personService.getPersons(); 257 | 258 | // then 259 | assertThat(persons).extracting(Person::getFirstname, Person::getLastname).containsExactly("Oliver", "Weiler"); 260 | } 261 | } 262 | ``` 263 | 264 | * Use [AssertJ](http://joel-costigliola.github.io/assertj/). Avoid [Hamcrest](http://hamcrest.org/). 265 | 266 | > Why? `AssertJ` is more actively developed, requires only one static import, and allows you to discover assertions through autocompletion. 267 | 268 | ```java 269 | // bad 270 | import static org.hamcrest.MatcherAssert.assertThat; 271 | import static org.hamcrest.Matchers.is; 272 | import static org.hamcrest.Matchers.not; 273 | import static org.hamcrest.Matchers.empty; 274 | 275 | assertThat(persons), is(not(empty()))); 276 | 277 | // good 278 | import static org.assertj.core.api.Assertions.assertThat; 279 | 280 | assertThat(persons).isNotEmpty(); 281 | ``` 282 | 283 | **[⬆ back to top](#table-of-contents)** 284 | --------------------------------------------------------------------------------