Your First Service
Now let's create your first EAF service using the ACCI EAF CLI. You'll learn how to generate a properly structured service following hexagonal architecture principles and understand the generated code.
๐ฏ What You'll Learnโ
- How to use the ACCI EAF CLI for service generation
- Understanding hexagonal architecture structure
- Domain, application, and infrastructure layer separation
- TDD setup and failing tests approach
- EAF SDK integrations
๐ง Using the ACCI EAF CLIโ
The ACCI EAF CLI is a powerful code generation tool that creates fully structured services following our architectural patterns.
Generate Your First Serviceโ
Let's create a user profile service:
# Generate a new service in the apps directory
nx run acci-eaf-cli:run -- --args="generate service user-profile"
You should see output similar to:
โ
Creating service directory: apps/user-profile
โ
Generating build.gradle.kts
โ
Generating project.json for Nx
โ
Creating source directory structure
โ
Generating domain layer files
โ
Generating application layer files
โ
Generating infrastructure layer files
โ
Generating test files
โ
Updating settings.gradle.kts
โ
Service 'user-profile' generated successfully!
Alternative: Generate in libs Directoryโ
For shared services, you can generate in the libs
directory:
# Generate a shared service in libs
nx run acci-eaf-cli:run -- --args="generate service notification-client --path=libs"
๐ Understanding the Generated Structureโ
Let's explore what the CLI generated for your user-profile
service:
apps/user-profile/
โโโ build.gradle.kts # Gradle build configuration
โโโ project.json # Nx project configuration
โโโ src/
โโโ main/
โ โโโ kotlin/com/axians/eaf/userprofile/
โ โ โโโ UserProfileApplication.kt # Spring Boot application
โ โ โโโ domain/ # Domain Layer
โ โ โ โโโ model/
โ โ โ โ โโโ SampleUserProfile.kt # Domain model
โ โ โ โโโ port/ # Domain ports
โ โ โโโ application/ # Application Layer
โ โ โ โโโ port/
โ โ โ โ โโโ input/ # Use cases (input ports)
โ โ โ โ โ โโโ SampleUserProfileUseCase.kt
โ โ โ โ โโโ output/ # Output ports
โ โ โ โโโ service/ # Application services
โ โ โ โโโ SampleUserProfileService.kt
โ โ โโโ infrastructure/ # Infrastructure Layer
โ โ โโโ adapter/
โ โ โ โโโ input/web/ # REST controllers
โ โ โ โ โโโ SampleUserProfileController.kt
โ โ โ โโโ output/persistence/ # Repository implementations
โ โ โโโ config/ # Configuration classes
โ โโโ resources/
โ โโโ application.yml # Application configuration
โโโ test/
โโโ kotlin/com/axians/eaf/userprofile/
โโโ application/service/
โ โโโ SampleUserProfileServiceTest.kt # Unit tests
โโโ architecture/
โโโ ArchitectureTest.kt # ArchUnit tests
๐๏ธ Hexagonal Architecture Explainedโ
The generated structure follows hexagonal architecture (ports and adapters) principles:
Domain Layer (Center)โ
// Domain model - pure business logic, no external dependencies
data class SampleUserProfile(
val id: String = UUID.randomUUID().toString(),
val name: String,
val email: String,
val createdAt: Instant = Instant.now(),
val updatedAt: Instant = Instant.now(),
) {
// Business rules and invariants go here
fun updateProfile(newName: String, newEmail: String): SampleUserProfile {
require(newName.isNotBlank()) { "Name cannot be blank" }
require(newEmail.contains("@")) { "Email must be valid" }
return copy(
name = newName,
email = newEmail,
updatedAt = Instant.now()
)
}
}
Application Layer (Orchestration)โ
// Input port - defines what the application can do
interface SampleUserProfileUseCase {
fun findById(id: String): SampleUserProfile?
fun create(name: String, email: String): SampleUserProfile
fun update(id: String, name: String, email: String): SampleUserProfile
}
// Application service - implements use cases
@Service
class SampleUserProfileService : SampleUserProfileUseCase {
override fun findById(id: String): SampleUserProfile? {
// Implementation will use output ports (repositories)
return SampleUserProfile(
id = id,
name = "Sample User",
email = "user@example.com"
)
}
override fun create(name: String, email: String): SampleUserProfile {
// Validation and business logic
val profile = SampleUserProfile(name = name, email = email)
// Would save via repository port
return profile
}
override fun update(id: String, name: String, email: String): SampleUserProfile {
// Find existing, update, and save
val existing = findById(id) ?: throw IllegalArgumentException("Profile not found")
return existing.updateProfile(name, email)
}
}
Infrastructure Layer (Adapters)โ
// Input adapter - REST API
@RestController
@RequestMapping("/api/v1/user-profiles")
class SampleUserProfileController(
private val userProfileUseCase: SampleUserProfileUseCase,
) {
@GetMapping("/{id}")
fun getUserProfile(@PathVariable id: String): ResponseEntity<SampleUserProfile> {
val profile = userProfileUseCase.findById(id)
return if (profile != null) {
ResponseEntity.ok(profile)
} else {
ResponseEntity.notFound().build()
}
}
@PostMapping
fun createUserProfile(@RequestBody request: CreateUserProfileRequest): ResponseEntity<SampleUserProfile> {
val profile = userProfileUseCase.create(request.name, request.email)
return ResponseEntity.status(HttpStatus.CREATED).body(profile)
}
@GetMapping("/health")
fun health(): ResponseEntity<Map<String, String>> {
return ResponseEntity.ok(mapOf("status" to "UP", "service" to "user-profile"))
}
}
data class CreateUserProfileRequest(
val name: String,
val email: String,
)
๐งช Test-Driven Development Setupโ
The generated service includes TDD-ready tests:
Unit Test Exampleโ
class SampleUserProfileServiceTest {
private val service = SampleUserProfileService()
@Test
fun `should create user profile with valid data`() {
// Given
val name = "John Doe"
val email = "john.doe@example.com"
// When
val result = service.create(name, email)
// Then
assertThat(result.name).isEqualTo(name)
assertThat(result.email).isEqualTo(email)
assertThat(result.id).isNotBlank()
assertThat(result.createdAt).isNotNull()
}
@Test
fun `should find user profile by id`() {
// Given
val id = "test-id"
// When
val result = service.findById(id)
// Then
assertThat(result).isNotNull()
assertThat(result!!.id).isEqualTo(id)
}
@Test
fun `should return null for non-existent profile`() {
// This test would fail initially (TDD red phase)
// Implement proper repository integration to make it pass
// Given
val nonExistentId = "non-existent-id"
// When
val result = service.findById(nonExistentId)
// Then
// This assertion will help guide proper implementation
assertThat(result).isNull()
}
}
Architecture Testsโ
class ArchitectureTest {
private val importedClasses = ClassFileImporter()
.importPackages("com.axians.eaf.userprofile")
@Test
fun `domain layer should not depend on infrastructure layer`() {
val rule = noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..")
.because("Domain layer must be independent of infrastructure concerns")
rule.check(importedClasses)
}
@Test
fun `application layer should not depend on infrastructure layer`() {
val rule = noClasses()
.that().resideInAPackage("..application..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..")
.because("Application layer should only depend on domain layer")
rule.check(importedClasses)
}
@Test
fun `infrastructure layer can depend on application and domain layers`() {
val rule = classes()
.that().resideInAPackage("..infrastructure..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage(
"..infrastructure..",
"..application..",
"..domain..",
"java..",
"kotlin..",
"org.springframework..",
"com.axians.eaf.."
)
rule.check(importedClasses)
}
}
โ๏ธ Configuration and Dependenciesโ
Build Configurationโ
The generated build.gradle.kts
includes:
plugins {
id("org.jetbrains.kotlin.jvm")
id("org.springframework.boot")
id("io.spring.dependency-management")
id("org.jlleitschuh.gradle.ktlint")
}
dependencies {
// Spring Boot
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
// EAF SDKs
implementation(project(":libs:eaf-core"))
implementation(project(":libs:eaf-eventing-sdk"))
implementation(project(":libs:eaf-eventsourcing-sdk"))
// Kotlin
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// Testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.mockk:mockk")
testImplementation("com.tngtech.archunit:archunit-junit5")
}
Application Configurationโ
The generated application.yml
includes EAF integrations:
spring:
application:
name: user-profile
# EAF Local Development Configuration
eaf:
eventing:
nats:
url: 'nats://localhost:4222'
# PostgreSQL Connection (template)
# spring:
# datasource:
# url: jdbc:postgresql://localhost:5432/eaf_db
# username: postgres
# password: password
๐ Build and Run Your Serviceโ
Build the Serviceโ
# Build your new service
nx build user-profile
# Run tests
nx test user-profile
# Check code formatting
nx run user-profile:ktlintCheck
Run the Serviceโ
# Start the service
nx run user-profile:run
# Alternative: Use bootRun for Spring Boot features
nx run user-profile:bootRun
Test the Serviceโ
# Test health endpoint
curl -s http://localhost:8080/actuator/health
# Test your API endpoints
curl -s http://localhost:8080/api/v1/user-profiles/health
# Create a user profile (this will use the sample implementation)
curl -X POST http://localhost:8080/api/v1/user-profiles \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "john.doe@example.com"}'
๐ฏ Key Concepts Recapโ
Hexagonal Architecture Benefitsโ
- Testability: Domain logic isolated from external concerns
- Flexibility: Easy to swap implementations (databases, message brokers)
- Maintainability: Clear separation of concerns
- Independent Development: Teams can work on different layers independently
Domain-Driven Design Elementsโ
- Domain Model:
SampleUserProfile
represents the business entity - Use Cases: Clear definition of what the application can do
- Business Rules: Validation and invariants in the domain model
- Ubiquitous Language: Method and variable names reflect business terminology
Test-Driven Developmentโ
- Failing Tests: Architecture tests and unit tests guide implementation
- Red-Green-Refactor: Start with red tests, make them green, then refactor
- Living Documentation: Tests describe expected behavior
- Confidence: Comprehensive tests enable safe refactoring
๐ง Customization Pointsโ
Before moving to the next step, consider these customization opportunities:
- Domain Model: Replace the sample with your actual business entity
- Use Cases: Define the specific operations your service needs
- Database Integration: Add JPA entities and repositories
- Event Publishing: Integrate NATS for event-driven communication
- Validation: Add proper input validation and error handling
โ Success Criteriaโ
You've successfully created your first EAF service if:
- โ Service generates without errors
- โ All tests pass (including architecture tests)
- โ Service starts successfully
- โ Health endpoint responds correctly
- โ You understand the hexagonal architecture structure
๐ Next Stepsโ
Now that you have a working service structure, let's implement a complete feature! Continue to Hello World Example to build a real user profile management system using TDD and DDD principles.
Great progress! You've created your first EAF service following hexagonal architecture. Next, we'll bring it to life with actual functionality! ๐