Skip to main content

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:

  1. Domain Model: Replace the sample with your actual business entity
  2. Use Cases: Define the specific operations your service needs
  3. Database Integration: Add JPA entities and repositories
  4. Event Publishing: Integrate NATS for event-driven communication
  5. 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! ๐ŸŽ‰