├── .gitignore
├── .gitattributes
├── src
└── main
│ ├── java
│ └── com
│ │ └── github
│ │ └── cleanrest
│ │ ├── core
│ │ ├── port
│ │ │ ├── GreetingInputPort.java
│ │ │ ├── GreetingPresenterOutputPort.java
│ │ │ └── GreetingGatewayOutputPort.java
│ │ ├── model
│ │ │ └── Greeting.java
│ │ └── usecase
│ │ │ └── GreetSomeoneUseCase.java
│ │ └── infrastucture
│ │ ├── CleanRestSpringBootApplication.java
│ │ ├── config
│ │ └── AppConfig.java
│ │ ├── controller
│ │ └── GreetingRestController.java
│ │ ├── gateway
│ │ └── InMemoryGreetingGateway.java
│ │ └── presenter
│ │ └── GreetingRestPresenter.java
│ └── resources
│ └── application.properties
├── README.md
└── pom.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .idea/
3 | target/
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 | *.png binary
3 | *.jpg binary
4 | *.gif binary
5 | *.jar binary
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/core/port/GreetingInputPort.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.core.port;
2 |
3 | public interface GreetingInputPort {
4 |
5 | void sayHello(String toWhom);
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.output.ansi.enabled=always
2 |
3 | # we want to see any errors as is
4 | server.error.whitelabel.enabled=false
5 | spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/core/port/GreetingPresenterOutputPort.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.core.port;
2 |
3 | import com.github.cleanrest.core.model.Greeting;
4 |
5 | public interface GreetingPresenterOutputPort {
6 | void presentGreeting(Greeting greeting);
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/core/model/Greeting.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.core.model;
2 |
3 | import lombok.Builder;
4 | import lombok.Getter;
5 |
6 |
7 | @Getter
8 | @Builder
9 | public class Greeting {
10 |
11 | String forWhom;
12 | String message;
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/core/port/GreetingGatewayOutputPort.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.core.port;
2 |
3 | import com.github.cleanrest.core.model.Greeting;
4 |
5 | public interface GreetingGatewayOutputPort {
6 |
7 | Greeting retrievePersonalGreeting(String forWhom);
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/infrastucture/CleanRestSpringBootApplication.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.infrastucture;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class CleanRestSpringBootApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(CleanRestSpringBootApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Spring REST and Clean architecture
2 | ---
3 |
4 | This is an example of how we can use Spring Web (REST) and Clean architectural patterns, especially
5 | when it comes to separating `Controller` and `Presenter`.
6 |
7 | This example was inspired by the discussion in this very interesting post:
8 |
9 | [Use case containing the presenter or returning data?](https://stackoverflow.com/questions/45921928/use-case-containing-the-presenter-or-returning-data)
10 |
11 | And, of course, here is the reference article by Robert C. Martin
12 |
13 | [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/core/usecase/GreetSomeoneUseCase.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.core.usecase;
2 |
3 | import com.github.cleanrest.core.port.GreetingGatewayOutputPort;
4 | import com.github.cleanrest.core.port.GreetingInputPort;
5 | import com.github.cleanrest.core.port.GreetingPresenterOutputPort;
6 | import lombok.RequiredArgsConstructor;
7 |
8 | @RequiredArgsConstructor
9 | public class GreetSomeoneUseCase implements GreetingInputPort {
10 |
11 | private final GreetingPresenterOutputPort greetingPresenter;
12 | private final GreetingGatewayOutputPort greetingGateway;
13 |
14 | @Override
15 | public void sayHello(String toWhom) {
16 |
17 | // This is completely abstracted (or "clean") logic:
18 | // get a greeting, present the greeting.
19 |
20 | greetingPresenter.presentGreeting(greetingGateway.retrievePersonalGreeting(toWhom));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/infrastucture/config/AppConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.infrastucture.config;
2 |
3 | import com.github.cleanrest.core.port.GreetingGatewayOutputPort;
4 | import com.github.cleanrest.core.port.GreetingInputPort;
5 | import com.github.cleanrest.core.usecase.GreetSomeoneUseCase;
6 | import com.github.cleanrest.infrastucture.presenter.GreetingRestPresenter;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Configuration;
9 | import org.springframework.context.annotation.Scope;
10 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
11 |
12 | import javax.servlet.http.HttpServletResponse;
13 |
14 | @Configuration
15 | public class AppConfig {
16 |
17 | @Bean(autowireCandidate = false)
18 | @Scope("request")
19 | public GreetingInputPort greetingRestUseCase(HttpServletResponse httpServletResponse,
20 | MappingJackson2HttpMessageConverter jacksonConverter,
21 | GreetingGatewayOutputPort greetingGateway) {
22 | return new GreetSomeoneUseCase(new GreetingRestPresenter(httpServletResponse, jacksonConverter),
23 | greetingGateway);
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.5.6
9 |
10 |
11 | com.gihub
12 | clean-rest
13 | 1.0.0
14 | clean-rest
15 | Demo project to show how Spring Web (REST) and Clean architecture can be combined
16 |
17 | 16
18 |
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-web
23 |
24 |
25 |
26 | org.projectlombok
27 | lombok
28 | true
29 |
30 |
31 | org.springframework.boot
32 | spring-boot-starter-test
33 | test
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/infrastucture/controller/GreetingRestController.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.infrastucture.controller;
2 |
3 | import com.github.cleanrest.core.port.GreetingInputPort;
4 | import lombok.RequiredArgsConstructor;
5 | import org.springframework.context.ApplicationContext;
6 | import org.springframework.web.bind.annotation.GetMapping;
7 | import org.springframework.web.bind.annotation.RequestParam;
8 | import org.springframework.web.bind.annotation.RestController;
9 |
10 | @RequiredArgsConstructor
11 | @RestController
12 | public class GreetingRestController {
13 |
14 | private final ApplicationContext applicationContext;
15 |
16 | @GetMapping("/greet")
17 | public void greet(@RequestParam String name) {
18 |
19 | /*
20 | We are getting our use case from the DI container, then we
21 | just execute the use case leaving it to decide for itself how
22 | it should present the results.
23 | */
24 |
25 | // Get an instance of the use case from the DI
26 | final GreetingInputPort greetingUseCase = applicationContext.getBean(GreetingInputPort.class);
27 |
28 | // Just fire and forget
29 | greetingUseCase.sayHello(name);
30 |
31 | // Notice that we are not returning anything here,
32 | // use case will tell the presenter what to present.
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/infrastucture/gateway/InMemoryGreetingGateway.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.infrastucture.gateway;
2 |
3 | import com.github.cleanrest.core.model.Greeting;
4 | import com.github.cleanrest.core.port.GreetingGatewayOutputPort;
5 | import org.springframework.stereotype.Service;
6 |
7 | import java.util.Map;
8 | import java.util.Optional;
9 | import java.util.stream.Collectors;
10 | import java.util.stream.Stream;
11 |
12 | @Service
13 | public class InMemoryGreetingGateway implements GreetingGatewayOutputPort {
14 |
15 | private static final Map greetings =
16 | Stream.of(new String[]{"George", "Hello %s"},
17 | new String[]{"Brad", "Hi %s, how are you?"})
18 | .collect(Collectors.toUnmodifiableMap(pair -> pair[0], pair -> pair[1]));
19 |
20 |
21 | @Override
22 | public Greeting retrievePersonalGreeting(String forWhom) {
23 | return Optional.ofNullable(greetings
24 | .get(forWhom))
25 | .map(template -> template.formatted(forWhom))
26 | .map(message -> Greeting.builder()
27 | .forWhom(forWhom)
28 | .message(message)
29 | .build())
30 | .orElse(Greeting.builder()
31 | .forWhom("Somebody")
32 | .message("Hi, stranger!")
33 | .build());
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/github/cleanrest/infrastucture/presenter/GreetingRestPresenter.java:
--------------------------------------------------------------------------------
1 | package com.github.cleanrest.infrastucture.presenter;
2 |
3 | import com.github.cleanrest.core.model.Greeting;
4 | import com.github.cleanrest.core.port.GreetingPresenterOutputPort;
5 | import lombok.RequiredArgsConstructor;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.http.MediaType;
8 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
9 | import org.springframework.http.server.DelegatingServerHttpResponse;
10 | import org.springframework.http.server.ServletServerHttpResponse;
11 |
12 | import javax.servlet.http.HttpServletResponse;
13 | import java.io.IOException;
14 |
15 | @RequiredArgsConstructor
16 | public class GreetingRestPresenter implements GreetingPresenterOutputPort {
17 |
18 | private final HttpServletResponse httpServletResponse;
19 | private final MappingJackson2HttpMessageConverter jacksonConverter;
20 |
21 | @Override
22 | public void presentGreeting(Greeting greeting) {
23 |
24 | /*
25 | Doing a bit of heavy lifting here ourselves:
26 | need to serialize the response model as JSON
27 | to the HTTP response. This will normally will
28 | be done by Spring Web when a request handling
29 | method returning the response model and annotated
30 | with ResponseBody.
31 | */
32 |
33 | // construct HTTP output message working with Servlet HTTP
34 | // response
35 | final DelegatingServerHttpResponse httpOutputMessage =
36 | new DelegatingServerHttpResponse(new ServletServerHttpResponse(httpServletResponse));
37 |
38 | // set status OK
39 | httpOutputMessage.setStatusCode(HttpStatus.OK);
40 |
41 | // serialize response model to JSON as the body of the message
42 | try {
43 | jacksonConverter.write(greeting, MediaType.APPLICATION_JSON, httpOutputMessage);
44 | } catch (IOException e) {
45 | // just for this example
46 | throw new RuntimeException(e);
47 | }
48 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------