Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added java modules #44

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 0 additions & 36 deletions .github/workflows/ci.yml

This file was deleted.

115 changes: 39 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,85 +1,48 @@
# Get Your Hands Dirty on Clean Architecture
# Spring Boot Hexagonal
Project shows, how to use hexagonal architecture in your spring boot applications

This repository implements a small web app in the Hexagonal Architecture style, as discussed in the book "Get Your Hands Dirty on Clean Architecture".

The code has been updated to the 2nd edition of the book.

## Get the print book

[![Get Your Hands Dirty on Clean Architecture cover](img/cover-packt-450.png)](https://www.amazon.com/Your-Hands-Dirty-Clean-Architecture/dp/180512837X?keywords=get+your+hands+dirty+on+clean+architecture&qid=1689324075&sprefix=Get+Your+Hands+Dirty+on+Clean+,aps,424&sr=8-2&_encoding=UTF8&tag=reflectorin0c-20&linkCode=ur2&linkId=c04a12e6dd6d399747b0cdce328650a5&camp=1789&creative=9325)

## Get the e-book

This is the self-published version, which is only available electronically.
![Hexagonal Architecture](img/hexagonal-architecture.png)

[![Get Your Hands Dirty on Clean Architecture cover](img/cover-430.png)](https://thombergs.gumroad.com/l/gyhdoca)

## Companion Articles
## Implemented integrations:
* Mysql (Spring Data JPA)
* Redis (Spring Data Redis)

* [Hexagonal Architecture with Java and Spring](https://reflectoring.io/spring-hexagonal/)
* [Building a Multi-Module Spring Boot Application with Gradle](https://reflectoring.io/spring-boot-gradle-multi-module/)

## Prerequisites

* JDK 17
* this project uses Lombok, so enable annotation processing in your IDE

## About the book
### All About Hexagonal Architecture

* Learn the concepts behind "Clean Architecture" and "Hexagonal Architecture".
* Explore a hands-on approach of implementing a Hexagonal architecture with example code [on GitHub](https://github.com/thombergs/buckpal).
* Develop your domain code independent of database or web concerns.

![Hexagonal Architecture](img/hexagonal-architecture.png)

### Get a Grip on Your Layers

* Learn about potential problems of the common layered architecture style.
* Free your domain layer of oppressive dependencies using dependency inversion.
* Structure your code in an architecturally expressive way.
* Use different methods to enforce architectural boundaries.
* Learn the consequences of shortcuts and when to accept them.
* ... and [more](#table-of-contents).

![Dependencies](img/dependencies.png)

### What Readers Say

> Tom Hombergs has done a terrific job in explaining clean architecture - from concepts to code. Really wish more technical books would be as clear as that one!

Gernot Starke - Fellow at [INNOQ](https://www.innoq.com/en/staff/gernot-starke/), Founder of [arc42](https://arc42.org/), Author of Software Architecture Books, Coach, and Consultant

> Love your book. One of the most practical books on hexagonal architecture I have seen/read so far.

Marten Deinum - Spring Framework Contributor and Author of ["Spring 5 Recipes"](https://www.amazon.com/Spring-5-Recipes-Problem-Solution-Approach/dp/1484227891&tag=reflectorin0c-20) and ["Spring Boot 2 Recipes"](https://www.amazon.com/Spring-Boot-Recipes-Problem-Solution-Approach/dp/1484239628&tag=reflectorin0c-20)

> A book taken right out of the machine room of software development. Tom talks straight from his experience and guides you through the day-to-day trade-offs necessary to deliver clean architecture.

Sebastian Kempken - Software Architect at Adcubum

> Thank you for the great book, it helped me gain significant insight into how one would go about implementing hexagonal and DDD in a modern Spring project.

Spyros Vallianos - Java Developer at Konnekt-able

> After reading it I had one of these 'aha' moments when things finally click in your brain.

Manos Tzagkarakis - Java Developer at Datawise

### Table of Contents

1. Maintainability
2. What's Wrong with Layers?
3. Inverting Dependencies
4. Organizing Code
5. Implementing a Use Case
6. Implementing a Web Adapter
7. Implementing a Persistence Adapter
8. Testing Architecture Elements
9. Mapping Between Boundaries
10. Assembling the Application
11. Taking Shortcuts Consciously
12. Enforcing Architecture Boundaries
13. Managing Multiple Bounded Contexts
14. A Component-Based Approach to Software Architecture
15. Deciding on an Architecture Style
* this project uses Testcontainers, so run Docker on your local machine

## Getting Started
`gradle testBootRun`

## Project Structure
```
└──com/
└── yourcompany/
├── adapter/ # Adapter logic
│ ├── in/ # Incoming requests adapters
│ │ └── http/
│ └── out/ # Outgoing requests adapters
│ ├── cache/
│ ├── kafka/
│ └── persistense/
├── application # Core logic
│ ├── domain/
│ │ ├── model/
│ │ └── service/
│ └── port/ # Core logic API
│ ├── in/
│ └── out/
└── common/ # Neither business logic nor adapters
```

## See More

* [Гексагональная Архитектура и Spring Boot](https://habr.com/ru/articles/795127/)
* Forked and inspired by [hombergs/buckpal](https://github.com/thombergs/buckpal)
* [YouTube: Рустам Ахметов — Архитектура приложения и ошибки проектирования](https://www.youtube.com/watch?v=X6QdWTE1HHw&t=2194s&ab_channel=JPoint%2CJoker%D0%B8JUGru)
* [Hexagonal Architecture with Java and Spring](https://reflectoring.io/spring-hexagonal/)
* [Building a Multi-Module Spring Boot Application with Gradle](https://reflectoring.io/spring-boot-gradle-multi-module/)
3 changes: 3 additions & 0 deletions application/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
id 'core.java-conventions'
}
7 changes: 7 additions & 0 deletions application/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module spring.boot.hexagonal.application {
exports nd.jar.springhexboot.application.port.in;
exports nd.jar.springhexboot.application.domain.model;
exports nd.jar.springhexboot.application.port.out;
requires static lombok;
requires spring.context;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nd.jar.springhexboot.application.domain.model;

public record Event(
String id,
String name,
String description,
String from
){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nd.jar.springhexboot.application.domain.service;

import lombok.RequiredArgsConstructor;
import nd.jar.springhexboot.application.domain.model.Event;
import nd.jar.springhexboot.application.port.in.FindEventsUseCase;
import nd.jar.springhexboot.application.port.in.PushEventUseCase;
import nd.jar.springhexboot.application.port.out.ExternalStorage;
import org.springframework.stereotype.Service;

import java.util.Map;

import static java.util.stream.Collectors.toMap;

@Service
@RequiredArgsConstructor
public class EventService implements PushEventUseCase, FindEventsUseCase {
private final Map<String, ExternalStorage> storages;
@Override
public boolean push(Event event) {
return storages.values().stream().map(sub -> sub.push(event))
.anyMatch(result -> !result);
}

@Override
public Map<String, Event> find(String eventId) {
return storages.entrySet().stream()
.collect(toMap(Map.Entry::getKey, entry -> entry.getValue().find(eventId))).entrySet().stream()
.filter(entry -> entry.getValue().isPresent())
.collect(toMap(Map.Entry::getKey, entry -> entry.getValue().get()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package nd.jar.springhexboot.application.port.in;

import nd.jar.springhexboot.application.domain.model.Event;

import java.util.Map;

public interface FindEventsUseCase {
Map<String, Event> find(String eventId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nd.jar.springhexboot.application.port.in;

import nd.jar.springhexboot.application.domain.model.Event;

public interface PushEventUseCase {
boolean push(Event event);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package nd.jar.springhexboot.application.port.out;

import nd.jar.springhexboot.application.domain.model.Event;

import java.util.Optional;

public interface ExternalStorage {
boolean push(Event event);
Optional<Event> find(String id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package nd.jar.springhexboot.application.port.out;

import nd.jar.springhexboot.application.domain.model.Event;

import java.util.Optional;

public interface GetEventPort {
Optional<Event> find(String id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package nd.jar.springhexboot.application.port.out;

import nd.jar.springhexboot.application.domain.model.Event;

import java.util.List;

public interface GetEventsPort {
List<Event> getAll();
}
25 changes: 25 additions & 0 deletions bootstrap/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id 'core.java-conventions'
id 'org.springframework.boot' version '3.2.5'
id 'application'
}
application {
mainClass = 'nd.jar.springhexboot.App'
}

dependencies {
implementation project(":application")
implementation project(":out.persistence")
implementation project(":in.http")
implementation project(":out.redis")
implementation project(":out.kafka")

implementation 'org.springframework.boot:spring-boot-autoconfigure'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation "org.springframework.boot:spring-boot-devtools"
testImplementation "org.testcontainers:mysql:1.19.0"
testImplementation "org.testcontainers:kafka:1.19.5"
testImplementation "com.redis:testcontainers-redis:2.0.1"
}
12 changes: 12 additions & 0 deletions bootstrap/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module spring.boot.hexagonal.bootstrap {
exports nd.jar.springhexboot;
requires spring.boot.hexagonal.application;
requires spring.boot.hexagonal.in.http;
requires spring.boot.hexagonal.out.persistence;
requires spring.boot.autoconfigure;
requires spring.boot;
requires spring.boot.hexagonal.out.redis;
requires spring.boot.hexagonal.out.kafka;

opens nd.jar.springhexboot to spring.core;
}
11 changes: 11 additions & 0 deletions bootstrap/src/main/java/nd/jar/springhexboot/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package nd.jar.springhexboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
20 changes: 20 additions & 0 deletions bootstrap/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
spring:
jpa:
hibernate:
ddl-auto: create
show-sql: true
logging:
level:
org.springframework.web.reactive.function.client.ExchangeFunctions: debug
org:
hibernate:
internal:
SessionImpl: DEBUG
SQL: INFO
type: TRACE
loader:
hql: TRACE
engine:
transaction:
internal:
TransactionImpl: DEBUG
11 changes: 11 additions & 0 deletions bootstrap/src/test/java/nd/jar/springhexboot/TestApp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package nd.jar.springhexboot;

import org.springframework.boot.SpringApplication;

public class TestApp {
public static void main(String[] args) {
SpringApplication.from(App::main)
.with(TestContainersConfiguration.class)
.run();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package nd.jar.springhexboot;

import com.redis.testcontainers.RedisContainer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
import org.springframework.boot.devtools.restart.RestartScope;

@TestConfiguration
public class TestContainersConfiguration {
@Bean
@RestartScope
@ServiceConnection
MySQLContainer<?> mySQLContainer() {
return new MySQLContainer<>("mysql:5.7.39");
}

@Bean
@RestartScope
@ServiceConnection(name = "redis")
RedisContainer redisContainer(){
return new RedisContainer(DockerImageName.parse("redis:6.2.6"));
}

@Bean
@RestartScope
@ServiceConnection
KafkaContainer kafka() {
return new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1"));
}

}
Loading