Backend Development with Spring
Table of Contents
General Notes
The purpose of this section is to give guidelines/best practices for design and implementation of a typical backend REST API. A typical REST API exposes HTTP endpoints over HTTP and uses JSON requests and responses. It typically uses a relational database and integrates with some other 3rd party APIs.
These guidelines are divided into 2 parts. The first section concentrates on the best practices of backend REST API development. The second section contains Spring Boot specifics on how to effectively implement the principles defined in section one with the Spring Boot framework and Java programming language.
Backend Development
API-First Approach
The API-first approach as an alternative to the code-first approach states that one should first define the API of an application and get a buy-in of all stakeholders. Only after that can the application be developed in several parallel streams. Main benefits of API-first approach are:
- Earlier validation
- Clear abstraction layer
- Decoupling dependencies
- Faster growth
- Freedom from constraints
The output of the API First Design phase should be an OpenAPI Specification document. This document can be designed using Swagger Editor (also available as a docker image). Once it is committed, other teams (frontend, QA) can start using it. Since the API specification represents a contract, several teams can work in parallel. Every change to the specification should be clearly communicated to other team. Backend server code and also client side code can be generated based on the OpenAPI Specification.
API specification is the deployable artifact.
API Design
There are several schools of API design. Whichever you choose, make sure that your API design is consistent. Most of them agree on the following:
- Use resource names as endpoint paths
- Use HTTP methods (GET, PUT, POST, DELETE) instead of verbs in endpoint paths
e.g.GET /users
instead ofGET /getUsers
- Use plural nouns rather than singular
e.g.GET /users/{id}
instead ofGET /user/{id}
- Nest hierarchical objects
e.g.GET /users/{id}/devices/{id}
- Allow filtering, sorting, and pagination
- Version your API
- Handle errors and use appropriate HTTP codes. Use 4xx error codes for client errors and 5xx error codes for server errors (HTTP status codes).
Please comply with those agreed upon principles.
References:
Architecture Best Practices
As a backend developer you are responsible for the architecture of the backend service you are developing. There are some generally agreed upon best practices that should be familiar with and which you should incorporate into your service. Those are:
Backend API with Spring Boot
Spring Boot is a backend framework that allows you to develop production-grade, stand-alone applications. As Spring Boot is based on the Java Spring framework, it inherits most Java features and with it the complete JVM ecosystem (3rd party libs). The app runs on top of a multi-threaded web application server like Tomcat or Jetty.
One of the main pros of using Spring Boot is the Java community which is mature and fully grown. The community is vast and experienced. When you choose Spring Boot, you indirectly choose long-term support and maintainability for your application.
Spring Boot has already solved most of the common challenges a modern cloud native application is facing. Therefore, a developer does not need to reinvent the wheel. Instead, they use one of the ready-made Spring Boot starters or 3rd party library that solves the problem.
Best Practice Project
A good source code tells more than 1000 words. Therefore, we maintain a best practice project for a Spring Boot backend development. It is available at: spring-boot-template. It is an implementation of the pet clinic project REST API, and it follows the best practices described in this page. Please check out the different branches that contain approaches to a problem solution.
Language
Java is still the default programming language for Spring Boot application development. Make sure you use one of the newer versions and update it on a regular basis. Kotlin is another alternative that is fully supported by Spring Boot. At this moment Java is still preferred over Kotlin. How to write quality Java code? The answer for this is beyond this section. The book Effective Java is a great resource with generally agreed upon Java good practices. Here is a great summary of the book.
IDE
IntelliJ Ultimate Edition is the preferred IDE for Spring Boot application development. Import and apply the style guide that is defined by the project. If there is no project style guide, use the IntelliJ default style guide.
Some recommended plugins:
- Lombok
- SonarLint
Project Creation
Head over to Spring Initializr to get a pom with the dependencies that most suit your project. When adding new dependencies, Spring Initializr should be you preferred choice.
Project Structure
It is recommended that the Spring Boot project is Maven based. Gradle is a valid alternative. Reasons for choosing Maven over Gradle:
- out of the box solutions with plugins (artifact publishing, releasing, docker image building)
- existing knowledge and experience backend developers have with it
- robust design
- large community
Code Structure
Write your code as if it were Lego blocks. This means that small units with clearly defined responsibilities and a well-designed API can be combined into higher level units. The coupling between those units should be as loose as possible.
Use Maven submodules to be able to reuse some parts of the code in other projects. A submodule can easily be extracted into its own library.
When organizing your code into packages, follow the package by feature approach. Keep the packages small (7 +- 2 classes).
For smaller projects spring 3 layer architecture is recommended. But if you want to experiment with some more advanced architecture like clean (hexagonal) architecture you are welcome to give it a try and share the experience.
It is highly recommended that you split the data model used on the web from the database model. This prevents accidentally breaking the API and enables more flexibility with designing web (DTO) model and the database model. The cost of using a mapper object is not too high. A manual mapping implementation is recommended over mapping tools.
When you write business logic (service layer), think of it like creating a factory wih conveyor belts. The domain objects are the resources that are processed in the service pipelines. Each service is like a work station that manipulates/creates new domain objects. With such mindset the design of the API is much easier. The domain objects only need to know about its state. When considering creating a new object vs changing the existing one, pick a new object because immutable objects are much easier to work with.
Database Integration
Some good practices when working with a DB:
- Use postgres + spring-boot-starter-jpa as the default DB option.
- Audit tables where needed.
- Entities should have natural keys where possible. Use combined key if needed, or a UUID.
- Inspect and optimize JPA generated SQL queries. Use projections, entity graphs, and other JPA optimization strategies.
- Avoid N+1 queries.
- Avoid full table scans. Make sure that the database has all the required indexes for searching in place.
- Prefer single column indexes over composite indexes.
- Set
spring.jpa.open-in-view=false
. - Transaction boundary should be on the service layer. It should be explicit. Please note that transaction is rolled back by default at any runtime exception.
- Handle transactions with care as race conditions and deadlocks are notoriously hard to debug.
- Use custom queries (
@Query
) instead of spring derived ones when the method name is getting too long. - Avoid native SQL queries.
- Avoid manipulation of data with
@Query
- Prefer soft delete over hard delete since it can cause unexpected locking.
Database Migration
We prefer Flyway over Liquibase as database migration tools.
Migration scripts have to be written in SQL and migrations should be done with Flyway. Using Liquibase on projects, due to our experience, can cause deadlock issues. One good example of how to migrate a database with Flyway is this guide.
We also have to decide which tool to use, meaning which tool will bring us faster migration with as little as possible additional load and recovery time. Also, migration changesets should be written in a backward-compatible way. Cleanup of old structures should happen in the next release.
Logging
- SLF4J
- uniform log pattern including traceId/spanId
- log to system out only. Ship logs to CloudWatch.
- even better to use Fluentd and push the logs to Elasticsearch if possible.
- mask sensitive data (check Security section)
Exception Handling
Exception handling in Spring is nicely explained in this article.
Highlights:
- Define you own exceptions extending
RuntimeException
- Define a global exception handler
@ControllerAdvice
bean that extendsResponseEntityExceptionHandler
- Define an error class. It should be compatible with povio standard error response structure
- A list of error codes returned by the API is also recommended.
- The global exception handler should convert the exceptions to the error object that is returned in the response body.
- Use appropriate 4xx and 5xx HTTP error codes
- Override
ResponseEntityExceptionHandler
methods if you need to fine tune default Spring exception handling
Security
Spring security is a broad and complex topic. It takes time to master it. This article explains it in an effective and developer friendly way. Use the spring boot security starter. If JWT authentication is good enough for the project, then implement it. One good example how to implement JWT token generation and authentication is this guide.
OAuth2 related guidelines are still a WIP. Check also OAuth2 reference
Another aspect of security is handling sensitive data. Make sure sensitive user data such as passwords are hashed and data such as bank account numbers are encrypted. When logging, make sure sensitive data is masked.
A security review is recommended before the project is shipped.
3rd Party API Integration
Some best practices when integrating with a 3rd party REST API:
- use SDK if available
- create a 3rd party API client submodule (like an in-house implementation of a given 3rd party API)
- the client implementation should be lenient. This way minor changes in the API do not break your client. Example: The client must be able to handle certain changes in the API, e.g. addition of a new field in the API’s response.
- if required and economically feasible, create a stub/simulator implementation of the 3rd party server
Testing
- Use Mockito and JUnit5, without any Spring dependencies. Prefer strict mocking and avoid any matching where applicable.
- Each unit test should be written as a standalone unit to increase readability, do not optimize code where it is not needed.
- Do not change code in order to write a unit test, i.e. change access modifiers on variables. In these cases refactor.
- Cover all important business cases with unit tests, especially for projects where test coverage is not defined.
- Spring integration tests that test the whole API should use only one application context. Therefore, they should
inherit from an
AbstractIntegration
test. Use Failsafe Maven plugin if you want to run integration tests after unit tests pass. - e2e blackbox tests against a running environment. Those tests can be implemented using postman/newman. Those tests can also verify that a new build does not break anything from before (regression tests).
Application Configuration
Environment specific configuration properties should be defined with SPRING_APPLICATION_JSON
env variable or java system
property
java -Dspring.application.json='{"name":"test"}' -jar myapp.jar
use configuration properties to enable/disable some functionalities instead of @Profile
Open Source Attribution
We are developing proprietary software. In it we depend on open source libraries. Therefore, we are not allowed to use any open source libraries that have a copyleft license. We also have to give attribution (i.e. mention the license of the library we depend on in the license folder). To give appropriate attribution do the following:
- create a
licenses
folder in your resources folder - run
mvnw dependency:tree
- for each non-test dependency on the first level of the output
- create a folder with name same as the artifactId of the dependency
- add an INFO file to this folder. The info file should contain information about the dependency web page (mvn central) and the licenses it is using.
- for each license copy the license text file into this folder
CI/CD
Work in progress:
- docker image is the deployable artifact
- docker compose for local development environment
- separate repository for deployment -> infrastructure as a code