Add the hexagonal module: Hexagonal Architecture (Ports and Adapters) in Spring Boot 4
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.ankurm.hexagonal.domain.Wallet;
|
||||
import com.ankurm.hexagonal.ports.WalletRepository;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* The real implementation of the core's outbound port -- the only class in this whole
|
||||
* application that translates between the domain's {@link Wallet} and JPA's
|
||||
* {@link WalletEntity}. If the persistence technology changed to MongoDB tomorrow, this is
|
||||
* the only class that would need to change; {@code WalletService} would not even recompile
|
||||
* differently, because it has never seen this class.
|
||||
*/
|
||||
@Repository
|
||||
public class JpaWalletRepositoryAdapter implements WalletRepository {
|
||||
|
||||
private final SpringDataWalletRepository jpa;
|
||||
|
||||
public JpaWalletRepositoryAdapter(SpringDataWalletRepository jpa) {
|
||||
this.jpa = jpa;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Wallet> findById(String walletId) {
|
||||
return jpa.findById(walletId).map(entity -> new Wallet(entity.getId(), entity.getBalanceCents()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet save(Wallet wallet) {
|
||||
WalletEntity entity = jpa.findById(wallet.id()).orElseGet(() -> new WalletEntity(wallet.id(), 0));
|
||||
entity.setBalanceCents(wallet.balanceCents());
|
||||
jpa.save(entity);
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
interface SpringDataWalletRepository extends JpaRepository<WalletEntity, String> {
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/** The JPA shape of a wallet. Deliberately a separate class from the domain's
|
||||
* {@code Wallet} -- this class exists to satisfy Hibernate (a no-arg constructor, mutable
|
||||
* fields), and the domain class exists to satisfy the business rules. Mixing the two would
|
||||
* mean every persistence concern leaks into the one class the core's tests are supposed to
|
||||
* be free of. */
|
||||
@Entity
|
||||
@Table(name = "wallets")
|
||||
public class WalletEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
private long balanceCents;
|
||||
|
||||
protected WalletEntity() {
|
||||
}
|
||||
|
||||
public WalletEntity(String id, long balanceCents) {
|
||||
this.id = id;
|
||||
this.balanceCents = balanceCents;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getBalanceCents() {
|
||||
return balanceCents;
|
||||
}
|
||||
|
||||
public void setBalanceCents(long balanceCents) {
|
||||
this.balanceCents = balanceCents;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import com.ankurm.hexagonal.domain.Wallet;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** A real H2 database, a real JPA EntityManager, and the adapter under test -- proving the
|
||||
* port's contract against something that actually persists, not a guess at what Hibernate
|
||||
* does. */
|
||||
@DataJpaTest
|
||||
class JpaWalletRepositoryAdapterTest {
|
||||
|
||||
@Autowired
|
||||
SpringDataWalletRepository jpa;
|
||||
|
||||
@Test
|
||||
void savingThenFindingRoundTripsThroughTheRealDatabase() {
|
||||
JpaWalletRepositoryAdapter adapter = new JpaWalletRepositoryAdapter(jpa);
|
||||
|
||||
adapter.save(new Wallet("w1", 2500));
|
||||
Wallet reloaded = adapter.findById("w1").orElseThrow();
|
||||
|
||||
System.out.println("Reloaded wallet w1 balance from H2: " + reloaded.balanceCents());
|
||||
assertThat(reloaded.balanceCents()).isEqualTo(2500);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findingAnUnknownIdReturnsEmpty() {
|
||||
JpaWalletRepositoryAdapter adapter = new JpaWalletRepositoryAdapter(jpa);
|
||||
|
||||
boolean present = adapter.findById("ghost").isPresent();
|
||||
|
||||
System.out.println("findById(\"ghost\").isPresent(): " + present);
|
||||
assertThat(present).isFalse();
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/** Test-only bootstrap class so @DataJpaTest has a @SpringBootConfiguration to find in this
|
||||
* package. This module has no real @SpringBootApplication of its own -- that belongs to the
|
||||
* app module, which is the only place all the adapters actually get wired together. */
|
||||
@SpringBootApplication
|
||||
class TestApplication {
|
||||
}
|
||||
Reference in New Issue
Block a user