├── .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 | --------------------------------------------------------------------------------