This tutorial will show how to create a REST application using the Onion Architecture and Spring Boot (Spring Core, Spring MVC and Spring Data). For simplicity it will use an in-memory database (Fongo) and expose only one REST endpoint. Also please note that this tutorial focuses on using the Onion Architecture in practise and intentionally does not cover very important aspects of developing production applications such as writing tests, packaging of Spring applications or REST API design.
The traditional three-layered architecture consists of:
- presentation layer
- application layer (also called business logic, logic or middle layer)
- data layer
The traditional three-layered architecture has downward dependencies - the presentation layer depends on the application layers and the application layer depends on the data layer and therefore, transitively, the presentation layer depends on the data layer. The dependencies of the downward layers are inherited by the upward layers, so if the data layer defines a dependency to a library (e.g. ORM library) this dependency will be inherited by the application (and presentation) layer. In a project where boundaries between the layers are not enforced it might lead to a situation where an ORM class (e.g. SQLException
) is propagated to the application (and presentation) layer. This introduces an coupling between the layers - your domain (and presentation) is no longer independent of the implementation of the data layer - whenever the implementation of the data layer change (e.g. you switch from JPA to Spring Data) you have to change the domain (and presentation) layer. The Onion Architecture is designed to prevent this problem.
The Onion Architecture is a variant of multi-layered architecture, which consists of:
- application core which consists of:
- domain model
- domain services
- application services
- infrastructure
Create directory onionarch
with pom.xml
with:
pom
packaging- a dependency to javax.inject:javax.inject:1
- a dependency to junit:junit:4.12
- Java 1.8 properties
The complete pom.xml
content:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.adamsiemion.onionarch</groupId>
<artifactId>onionarch</artifactId>
<packaging>pom</packaging>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Multimodule maven project allows better dependencies management because each maven module can contain only the dependencies needed by the code in this specific module. Whenever the domain module requires access to infrastructure code, e.g. to send an email or download a file from FTP instead of adding a dependency to the selected infrastructure library in the domain layer one should:
- create an interface in the domain layer simplifying the API of the infrastructure library (Facade design pattern)
- create a new maven module with an implementation of the interface and dependencies to the chosen libraries
The Onion Architecture relies on the Dependency Inversion principle, so a way to specify that a class will be injected by the Dependency Injection framework is needed. One option is to use the annotations provided by the DI framework (e.g. Spring), however this will couple the domain to a specific infrastructure library. In order to prevent this coupling we use the annotations from the standard dependency injection API (JSR-330) javax.inject
.
From the root directory run:
mvn archetype:generate -DgroupId=com.github.adamsiemion.onionarch -DartifactId=onionarch-domain \
-DinteractiveMode=false -Dversion=1.0.0-SNAPSHOT
We start development from the domain layer, following the principles of Domain Driven Design.
A specific version (1.0.0-SNAPSHOT
) was provided just to follow the most popular versioning convention - semantic versioning.
rm -rf onionarch-domain\src\main\java\com onionarch-domain\src\test\java\com
Create class User
in onionarch-domain\src\main\java\com\github\adamsiemion\onionarch
public class User {
}
From the root directory run:
mvn archetype:generate -DgroupId=com.github.adamsiemion.onionarch -DartifactId=onionarch-rest \
-DinteractiveMode=false -Dversion=1.0.0-SNAPSHOT
rm -rf onionarch-rest\src\main\java\com onionarch-rest\src\test\java\com
Add below content to onionarch-rest\pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.3.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependency>
<groupId>com.github.adamsiemion.onionarch</groupId>
<artifactId>onionarch-domain</artifactId>
<version>${project.version}</version>
</dependency>
Edit pom.xml from the rest module directory and add:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Create class Application
in onionarch-rest\src\main\java\com\github\adamsiemion\onionarch
with the following content:
package com.github.adamsiemion.onionarch;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Create class UserRest
in onionarch-rest\src\main\java\com\github\adamsiemion\onionarch
with the following content:
package com.github.adamsiemion.onionarch;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserRest {
@RequestMapping(method = RequestMethod.GET)
public List<User> list() {
return new ArrayList<>();
}
}
If you build and run the application now and send a GET request to http://localhost:8080/users (curl http://localhost:8080/users
) the application will respond with an empty array.
- String id
- String name
The complete User
source code:
package com.github.adamsiemion.onionarch;
import java.util.Objects;
public class User {
private String id;
private String name;
User() {
}
public User(String name) {
this.name = name;
}
public User(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public String toString() {
return "User{id='" + id + "', name='" + name + "'}";
}
}
Lombok can reduce the number of boilerplate code (such as getters, toString()
, equals()
, hashCode()
).
It is possible to make the above class immutable what brings a lot of advantages, by defining an all args constructor and using Jackon’s parameter names module.
Create interface UserRepository
in onionarch-domain\src\main\java\com\github\adamsiemion\onionarch
with the following content:
package com.github.adamsiemion.onionarch;
public interface UserRepository {
Iterable<User> list();
User get(Long id);
void save(User user);
void delete(Long id);
}
Add the following content to onionarch-rest\src\main\java\com\github\adamsiemion\onionarch\UserRest.java
:
private final UserRepository userRepository;
@Inject
public UserRest(final UserRepository userRepository) {
this.userRepository = userRepository;
}
Add the following content to onionarch-rest\src\main\java\com\github\adamsiemion\onionarch\UserRest.java
(overwrite the existing list
method):
@RequestMapping(method = RequestMethod.GET)
public Iterable<User> list() {
return userRepository.list();
}
@RequestMapping(method = RequestMethod.POST)
public void create(@RequestBody User user) {
userRepository.save(user);
}
@RequestMapping(value = "{id}", method = RequestMethod.DELETE)
public void delete(@PathVariable("id") final Long id) {
userRepository.delete(id);
}
@RequestMapping(value = "{id}", method = RequestMethod.GET)
public User get(@PathVariable("id") final Long id) {
return userRepository.get(id);
}
Create class UserRespositoryFake
in onionarch-domain\src\main\java\com\github\adamsiemion\onionarch
with the following content:
package com.github.adamsiemion.onionarch;
import javax.inject.Named;
import java.util.Arrays;
import java.util.List;
@Named
public class UserRepositoryFake implements UserRepository {
@Override
public List<User> list() {
return Arrays.asList(new User(1L, "John Smith"), new User(2L, "John Doe"));
}
@Override
public User get(Long id) {
return new User();
}
@Override
public void save(User user) { }
@Override
public void delete(Long aLong) { }
}
This class is a fake implementation, created to test the current solution, which will not be used in production.
If you build and run the application now and send a GET request to http://localhost:8080/users (curl http://localhost:8080/users
) the application will respond with:
[{"id":1,"name":"John Smith"},{"id":2,"name":"John Doe"}]
From the root directory run:
mvn archetype:generate -DgroupId=com.github.adamsiemion.onionarch -DartifactId=onionarch-data \
-DinteractiveMode=false -Dversion=1.0.0-SNAPSHOT
<<<<<<< HEAD
rm -rf onionarch-data\src\main\java\com onionarch-data\src\test\java\com
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.3.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
<dependency>
<groupId>com.github.adamsiemion.onionarch</groupId>
<artifactId>onionarch-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.fakemongo</groupId>
<artifactId>fongo</artifactId>
<version>1.6.7</version>
</dependency>
<dependency>
<groupId>com.github.adamsiemion.onionarch</groupId>
<artifactId>onionarch-data</artifactId>
<version>${project.version}</version>
<type>runtime</type>
</dependency>
This is required because we want the Dependency Injection container to instantiate classes from the data layer in runtime but we do not want these classes at compile time.
Create class UserDaoMongo
in onionarch-data\src\main\java\com\github\adamsiemion\onionarch
with the following content:
package com.github.adamsiemion.onionarch;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface UserDaoMongo extends MongoRepository<User, String> {
}
Create class MongoConfig
in onionarch-data\src\main\java\com\github\adamsiemion\onionarch
with the following content:
package com.github.adamsiemion.onionarch;
import com.github.fakemongo.Fongo;
import com.mongodb.Mongo;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
@Configuration
public class MongoConfig extends AbstractMongoConfiguration {
@Override
protected String getDatabaseName() {
return "users";
}
@Override
public Mongo mongo() {
return new Fongo("mongo-test").getMongo();
}
}
Create class UserRepositorySpringData
in onionarch-data\src\main\java\com\github\adamsiemion\onionarch
with the following content:
package com.github.adamsiemion.onionarch;
import org.springframework.stereotype.Repository;
import javax.inject.Inject;
@Repository
public class UserRepositorySpringData implements UserRepository {
private final UserDaoMongo dao;
@Inject
public UserRepositorySpringData(final UserDaoMongo dao) {
this.dao = dao;
}
@Override
public Iterable<User> list() {
return dao.findAll();
}
@Override
public User get(String id) {
return dao.findOne(id);
}
@Override
public void save(User user) {
dao.save(user);
}
@Override
public void delete(String id) {
dao.delete(id);
}
}
The above class is an example of the delegate design pattern.
To build the application go to the root directory and run: mvn install
To run the application go to the root directory and run: java -jar onionarch-rest/target/onionarch-rest-1.0.0-SNAPSHOT.jar
curl http://localhost:8080/users
curl -H 'Content-Type: application/json' -X POST -d '{"name":"John Smith"}' http://localhost:8080/users
curl http://localhost:8080/users/<user_id>
curl -X DELETE http://localhost:8080/users/<user_id>