Hello World Example
Now let's implement a complete user profile management feature following TDD, DDD, and CQRS/ES principles. This hands-on example will transform your generated service into a fully functional system.
๐ฏ What You'll Buildโ
A User Profile Management system with:
- Domain:
UserProfile
aggregate with creation and update operations - Commands:
CreateUserProfileCommand
,UpdateUserProfileCommand
- Events:
UserProfileCreatedEvent
,UserProfileUpdatedEvent
- REST API: CRUD operations with proper error handling
- Event Publishing: NATS integration for event-driven communication
- Testing: Comprehensive TDD approach with unit and integration tests
๐ Prerequisitesโ
Ensure you have:
- โ Completed Your First Service
- โ Infrastructure services running (NATS, PostgreSQL)
- โ
user-profile
service generated and building
๐ด Phase 1: TDD - Write Failing Testsโ
Let's start with the TDD approach - Red phase first!
Domain Model Testsโ
Replace the generated test file with a comprehensive domain test:
// src/test/kotlin/com/axians/eaf/userprofile/domain/model/UserProfileTest.kt
package com.axians.eaf.userprofile.domain.model
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Test
import java.time.Instant
class UserProfileTest {
@Test
fun `should create user profile with valid data`() {
// Given
val name = "John Doe"
val email = "john.doe@example.com"
val bio = "Software Developer"
// When
val profile = UserProfile(
name = name,
email = email,
bio = bio
)
// Then
assertThat(profile.id).isNotBlank()
assertThat(profile.name).isEqualTo(name)
assertThat(profile.email).isEqualTo(email)
assertThat(profile.bio).isEqualTo(bio)
assertThat(profile.createdAt).isNotNull()
assertThat(profile.updatedAt).isNotNull()
assertThat(profile.isActive).isTrue()
}
@Test
fun `should reject blank name`() {
assertThatThrownBy {
UserProfile(name = "", email = "test@example.com", bio = "Bio")
}.isInstanceOf(IllegalArgumentException::class.java)
.hasMessageContaining("Name cannot be blank")
}
@Test
fun `should reject invalid email`() {
assertThatThrownBy {
UserProfile(name = "John", email = "invalid-email", bio = "Bio")
}.isInstanceOf(IllegalArgumentException::class.java)
.hasMessageContaining("Email must be valid")
}
@Test
fun `should update profile successfully`() {
// Given
val profile = UserProfile(
name = "John Doe",
email = "john@example.com",
bio = "Developer"
)
val originalUpdatedAt = profile.updatedAt
// Wait to ensure timestamp difference
Thread.sleep(10)
// When
val updatedProfile = profile.updateProfile(
name = "Jane Doe",
email = "jane@example.com",
bio = "Senior Developer"
)
// Then
assertThat(updatedProfile.id).isEqualTo(profile.id)
assertThat(updatedProfile.name).isEqualTo("Jane Doe")
assertThat(updatedProfile.email).isEqualTo("jane@example.com")
assertThat(updatedProfile.bio).isEqualTo("Senior Developer")
assertThat(updatedProfile.updatedAt).isAfter(originalUpdatedAt)
assertThat(updatedProfile.createdAt).isEqualTo(profile.createdAt)
}
@Test
fun `should deactivate profile`() {
// Given
val profile = UserProfile(
name = "John Doe",
email = "john@example.com",
bio = "Developer"
)
// When
val deactivatedProfile = profile.deactivate()
// Then
assertThat(deactivatedProfile.isActive).isFalse()
assertThat(deactivatedProfile.updatedAt).isAfter(profile.updatedAt)
}
}
Command and Event Testsโ
Create tests for CQRS components:
// src/test/kotlin/com/axians/eaf/userprofile/domain/command/UserProfileCommandTest.kt
package com.axians.eaf.userprofile.domain.command
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Test
class UserProfileCommandTest {
@Test
fun `should create valid CreateUserProfileCommand`() {
// Given
val name = "John Doe"
val email = "john@example.com"
val bio = "Developer"
// When
val command = CreateUserProfileCommand(
name = name,
email = email,
bio = bio
)
// Then
assertThat(command.name).isEqualTo(name)
assertThat(command.email).isEqualTo(email)
assertThat(command.bio).isEqualTo(bio)
}
@Test
fun `should create valid UpdateUserProfileCommand`() {
// Given
val id = "test-id"
val name = "Jane Doe"
val email = "jane@example.com"
val bio = "Senior Developer"
// When
val command = UpdateUserProfileCommand(
id = id,
name = name,
email = email,
bio = bio
)
// Then
assertThat(command.id).isEqualTo(id)
assertThat(command.name).isEqualTo(name)
assertThat(command.email).isEqualTo(email)
assertThat(command.bio).isEqualTo(bio)
}
}
Application Service Testsโ
Update the application service test:
// src/test/kotlin/com/axians/eaf/userprofile/application/service/UserProfileServiceTest.kt
package com.axians.eaf.userprofile.application.service
import com.axians.eaf.userprofile.application.port.output.UserProfileRepository
import com.axians.eaf.userprofile.application.port.output.UserProfileEventPublisher
import com.axians.eaf.userprofile.domain.command.CreateUserProfileCommand
import com.axians.eaf.userprofile.domain.command.UpdateUserProfileCommand
import com.axians.eaf.userprofile.domain.model.UserProfile
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class UserProfileServiceTest {
private val repository = mockk<UserProfileRepository>()
private val eventPublisher = mockk<UserProfileEventPublisher>()
private val service = UserProfileService(repository, eventPublisher)
@BeforeEach
fun setUp() {
every { eventPublisher.publishUserProfileCreated(any()) } returns Unit
every { eventPublisher.publishUserProfileUpdated(any()) } returns Unit
}
@Test
fun `should create user profile successfully`() {
// Given
val command = CreateUserProfileCommand(
name = "John Doe",
email = "john@example.com",
bio = "Developer"
)
every { repository.save(any()) } returnsArgument 0
// When
val result = service.createUserProfile(command)
// Then
assertThat(result.name).isEqualTo(command.name)
assertThat(result.email).isEqualTo(command.email)
assertThat(result.bio).isEqualTo(command.bio)
verify { repository.save(any()) }
verify { eventPublisher.publishUserProfileCreated(any()) }
}
@Test
fun `should find user profile by id`() {
// Given
val id = "test-id"
val profile = UserProfile(
id = id,
name = "John Doe",
email = "john@example.com",
bio = "Developer"
)
every { repository.findById(id) } returns profile
// When
val result = service.findUserProfileById(id)
// Then
assertThat(result).isEqualTo(profile)
verify { repository.findById(id) }
}
@Test
fun `should throw exception when profile not found`() {
// Given
val id = "non-existent-id"
every { repository.findById(id) } returns null
// When & Then
assertThatThrownBy { service.findUserProfileById(id) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessageContaining("User profile not found")
}
@Test
fun `should update user profile successfully`() {
// Given
val existingProfile = UserProfile(
id = "test-id",
name = "John Doe",
email = "john@example.com",
bio = "Developer"
)
val command = UpdateUserProfileCommand(
id = "test-id",
name = "Jane Doe",
email = "jane@example.com",
bio = "Senior Developer"
)
every { repository.findById("test-id") } returns existingProfile
every { repository.save(any()) } returnsArgument 0
// When
val result = service.updateUserProfile(command)
// Then
assertThat(result.name).isEqualTo(command.name)
assertThat(result.email).isEqualTo(command.email)
assertThat(result.bio).isEqualTo(command.bio)
verify { repository.save(any()) }
verify { eventPublisher.publishUserProfileUpdated(any()) }
}
}
๐ข Phase 2: TDD - Make Tests Pass (Green)โ
Now let's implement the domain model and make our tests pass:
Domain Model Implementationโ
// src/main/kotlin/com/axians/eaf/userprofile/domain/model/UserProfile.kt
package com.axians.eaf.userprofile.domain.model
import java.time.Instant
import java.util.*
data class UserProfile(
val id: String = UUID.randomUUID().toString(),
val name: String,
val email: String,
val bio: String,
val isActive: Boolean = true,
val createdAt: Instant = Instant.now(),
val updatedAt: Instant = Instant.now(),
) {
init {
require(name.isNotBlank()) { "Name cannot be blank" }
require(email.contains("@") && email.contains(".")) { "Email must be valid" }
require(bio.isNotBlank()) { "Bio cannot be blank" }
}
fun updateProfile(name: String, email: String, bio: String): UserProfile {
return copy(
name = name,
email = email,
bio = bio,
updatedAt = Instant.now()
)
}
fun deactivate(): UserProfile {
return copy(
isActive = false,
updatedAt = Instant.now()
)
}
fun activate(): UserProfile {
return copy(
isActive = true,
updatedAt = Instant.now()
)
}
}
Commands and Eventsโ
// src/main/kotlin/com/axians/eaf/userprofile/domain/command/UserProfileCommands.kt
package com.axians.eaf.userprofile.domain.command
data class CreateUserProfileCommand(
val name: String,
val email: String,
val bio: String,
)
data class UpdateUserProfileCommand(
val id: String,
val name: String,
val email: String,
val bio: String,
)
data class DeactivateUserProfileCommand(
val id: String,
)
// src/main/kotlin/com/axians/eaf/userprofile/domain/event/UserProfileEvents.kt
package com.axians.eaf.userprofile.domain.event
import java.time.Instant
data class UserProfileCreatedEvent(
val id: String,
val name: String,
val email: String,
val bio: String,
val createdAt: Instant,
)
data class UserProfileUpdatedEvent(
val id: String,
val name: String,
val email: String,
val bio: String,
val updatedAt: Instant,
)
data class UserProfileDeactivatedEvent(
val id: String,
val deactivatedAt: Instant,
)
Output Portsโ
// src/main/kotlin/com/axians/eaf/userprofile/application/port/output/UserProfileRepository.kt
package com.axians.eaf.userprofile.application.port.output
import com.axians.eaf.userprofile.domain.model.UserProfile
interface UserProfileRepository {
fun save(userProfile: UserProfile): UserProfile
fun findById(id: String): UserProfile?
fun findByEmail(email: String): UserProfile?
fun findAll(): List<UserProfile>
fun delete(id: String)
}
// src/main/kotlin/com/axians/eaf/userprofile/application/port/output/UserProfileEventPublisher.kt
package com.axians.eaf.userprofile.application.port.output
import com.axians.eaf.userprofile.domain.event.UserProfileCreatedEvent
import com.axians.eaf.userprofile.domain.event.UserProfileUpdatedEvent
import com.axians.eaf.userprofile.domain.event.UserProfileDeactivatedEvent
interface UserProfileEventPublisher {
fun publishUserProfileCreated(event: UserProfileCreatedEvent)
fun publishUserProfileUpdated(event: UserProfileUpdatedEvent)
fun publishUserProfileDeactivated(event: UserProfileDeactivatedEvent)
}
Input Port (Use Case)โ
// src/main/kotlin/com/axians/eaf/userprofile/application/port/input/UserProfileUseCase.kt
package com.axians.eaf.userprofile.application.port.input
import com.axians.eaf.userprofile.domain.command.CreateUserProfileCommand
import com.axians.eaf.userprofile.domain.command.UpdateUserProfileCommand
import com.axians.eaf.userprofile.domain.command.DeactivateUserProfileCommand
import com.axians.eaf.userprofile.domain.model.UserProfile
interface UserProfileUseCase {
fun createUserProfile(command: CreateUserProfileCommand): UserProfile
fun findUserProfileById(id: String): UserProfile
fun findUserProfileByEmail(email: String): UserProfile
fun updateUserProfile(command: UpdateUserProfileCommand): UserProfile
fun deactivateUserProfile(command: DeactivateUserProfileCommand): UserProfile
fun getAllUserProfiles(): List<UserProfile>
}
Application Service Implementationโ
// src/main/kotlin/com/axians/eaf/userprofile/application/service/UserProfileService.kt
package com.axians.eaf.userprofile.application.service
import com.axians.eaf.userprofile.application.port.input.UserProfileUseCase
import com.axians.eaf.userprofile.application.port.output.UserProfileRepository
import com.axians.eaf.userprofile.application.port.output.UserProfileEventPublisher
import com.axians.eaf.userprofile.domain.command.CreateUserProfileCommand
import com.axians.eaf.userprofile.domain.command.UpdateUserProfileCommand
import com.axians.eaf.userprofile.domain.command.DeactivateUserProfileCommand
import com.axians.eaf.userprofile.domain.event.UserProfileCreatedEvent
import com.axians.eaf.userprofile.domain.event.UserProfileUpdatedEvent
import com.axians.eaf.userprofile.domain.event.UserProfileDeactivatedEvent
import com.axians.eaf.userprofile.domain.model.UserProfile
import org.springframework.stereotype.Service
@Service
class UserProfileService(
private val repository: UserProfileRepository,
private val eventPublisher: UserProfileEventPublisher,
) : UserProfileUseCase {
override fun createUserProfile(command: CreateUserProfileCommand): UserProfile {
// Create domain object
val userProfile = UserProfile(
name = command.name,
email = command.email,
bio = command.bio
)
// Save to repository
val savedProfile = repository.save(userProfile)
// Publish event
eventPublisher.publishUserProfileCreated(
UserProfileCreatedEvent(
id = savedProfile.id,
name = savedProfile.name,
email = savedProfile.email,
bio = savedProfile.bio,
createdAt = savedProfile.createdAt
)
)
return savedProfile
}
override fun findUserProfileById(id: String): UserProfile {
return repository.findById(id)
?: throw IllegalArgumentException("User profile not found with id: $id")
}
override fun findUserProfileByEmail(email: String): UserProfile {
return repository.findByEmail(email)
?: throw IllegalArgumentException("User profile not found with email: $email")
}
override fun updateUserProfile(command: UpdateUserProfileCommand): UserProfile {
val existingProfile = findUserProfileById(command.id)
val updatedProfile = existingProfile.updateProfile(
name = command.name,
email = command.email,
bio = command.bio
)
val savedProfile = repository.save(updatedProfile)
// Publish event
eventPublisher.publishUserProfileUpdated(
UserProfileUpdatedEvent(
id = savedProfile.id,
name = savedProfile.name,
email = savedProfile.email,
bio = savedProfile.bio,
updatedAt = savedProfile.updatedAt
)
)
return savedProfile
}
override fun deactivateUserProfile(command: DeactivateUserProfileCommand): UserProfile {
val existingProfile = findUserProfileById(command.id)
val deactivatedProfile = existingProfile.deactivate()
val savedProfile = repository.save(deactivatedProfile)
// Publish event
eventPublisher.publishUserProfileDeactivated(
UserProfileDeactivatedEvent(
id = savedProfile.id,
deactivatedAt = savedProfile.updatedAt
)
)
return savedProfile
}
override fun getAllUserProfiles(): List<UserProfile> {
return repository.findAll()
}
}
๐ง Phase 3: Infrastructure Implementationโ
In-Memory Repository (for simplicity)โ
// src/main/kotlin/com/axians/eaf/userprofile/infrastructure/adapter/output/persistence/InMemoryUserProfileRepository.kt
package com.axians.eaf.userprofile.infrastructure.adapter.output.persistence
import com.axians.eaf.userprofile.application.port.output.UserProfileRepository
import com.axians.eaf.userprofile.domain.model.UserProfile
import org.springframework.stereotype.Repository
import java.util.concurrent.ConcurrentHashMap
@Repository
class InMemoryUserProfileRepository : UserProfileRepository {
private val profiles = ConcurrentHashMap<String, UserProfile>()
override fun save(userProfile: UserProfile): UserProfile {
profiles[userProfile.id] = userProfile
return userProfile
}
override fun findById(id: String): UserProfile? {
return profiles[id]
}
override fun findByEmail(email: String): UserProfile? {
return profiles.values.find { it.email == email }
}
override fun findAll(): List<UserProfile> {
return profiles.values.toList()
}
override fun delete(id: String) {
profiles.remove(id)
}
}
NATS Event Publisherโ
// src/main/kotlin/com/axians/eaf/userprofile/infrastructure/adapter/output/messaging/NatsUserProfileEventPublisher.kt
package com.axians.eaf.userprofile.infrastructure.adapter.output.messaging
import com.axians.eaf.eventing.NatsEventPublisher
import com.axians.eaf.userprofile.application.port.output.UserProfileEventPublisher
import com.axians.eaf.userprofile.domain.event.UserProfileCreatedEvent
import com.axians.eaf.userprofile.domain.event.UserProfileUpdatedEvent
import com.axians.eaf.userprofile.domain.event.UserProfileDeactivatedEvent
import org.springframework.stereotype.Component
@Component
class NatsUserProfileEventPublisher(
private val natsEventPublisher: NatsEventPublisher,
) : UserProfileEventPublisher {
override fun publishUserProfileCreated(event: UserProfileCreatedEvent) {
natsEventPublisher.publish(
subject = "eaf.user_profile.created",
tenantId = "default", // In real app, get from security context
event = event
)
}
override fun publishUserProfileUpdated(event: UserProfileUpdatedEvent) {
natsEventPublisher.publish(
subject = "eaf.user_profile.updated",
tenantId = "default",
event = event
)
}
override fun publishUserProfileDeactivated(event: UserProfileDeactivatedEvent) {
natsEventPublisher.publish(
subject = "eaf.user_profile.deactivated",
tenantId = "default",
event = event
)
}
}
REST Controllerโ
// src/main/kotlin/com/axians/eaf/userprofile/infrastructure/adapter/input/web/UserProfileController.kt
package com.axians.eaf.userprofile.infrastructure.adapter.input.web
import com.axians.eaf.userprofile.application.port.input.UserProfileUseCase
import com.axians.eaf.userprofile.domain.command.CreateUserProfileCommand
import com.axians.eaf.userprofile.domain.command.UpdateUserProfileCommand
import com.axians.eaf.userprofile.domain.command.DeactivateUserProfileCommand
import com.axians.eaf.userprofile.domain.model.UserProfile
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/v1/user-profiles")
class UserProfileController(
private val userProfileUseCase: UserProfileUseCase,
) {
@PostMapping
fun createUserProfile(@RequestBody request: CreateUserProfileRequest): ResponseEntity<UserProfile> {
val command = CreateUserProfileCommand(
name = request.name,
email = request.email,
bio = request.bio
)
val profile = userProfileUseCase.createUserProfile(command)
return ResponseEntity.status(HttpStatus.CREATED).body(profile)
}
@GetMapping("/{id}")
fun getUserProfile(@PathVariable id: String): ResponseEntity<UserProfile> {
return try {
val profile = userProfileUseCase.findUserProfileById(id)
ResponseEntity.ok(profile)
} catch (e: IllegalArgumentException) {
ResponseEntity.notFound().build()
}
}
@GetMapping
fun getAllUserProfiles(): ResponseEntity<List<UserProfile>> {
val profiles = userProfileUseCase.getAllUserProfiles()
return ResponseEntity.ok(profiles)
}
@PutMapping("/{id}")
fun updateUserProfile(
@PathVariable id: String,
@RequestBody request: UpdateUserProfileRequest
): ResponseEntity<UserProfile> {
return try {
val command = UpdateUserProfileCommand(
id = id,
name = request.name,
email = request.email,
bio = request.bio
)
val profile = userProfileUseCase.updateUserProfile(command)
ResponseEntity.ok(profile)
} catch (e: IllegalArgumentException) {
ResponseEntity.notFound().build()
}
}
@DeleteMapping("/{id}")
fun deactivateUserProfile(@PathVariable id: String): ResponseEntity<UserProfile> {
return try {
val command = DeactivateUserProfileCommand(id = id)
val profile = userProfileUseCase.deactivateUserProfile(command)
ResponseEntity.ok(profile)
} catch (e: IllegalArgumentException) {
ResponseEntity.notFound().build()
}
}
@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,
val bio: String,
)
data class UpdateUserProfileRequest(
val name: String,
val email: String,
val bio: String,
)
๐งช Phase 4: Integration Testingโ
Create a comprehensive integration test:
// src/test/kotlin/com/axians/eaf/userprofile/integration/UserProfileIntegrationTest.kt
package com.axians.eaf.userprofile.integration
import com.axians.eaf.userprofile.infrastructure.adapter.input.web.CreateUserProfileRequest
import com.axians.eaf.userprofile.infrastructure.adapter.input.web.UpdateUserProfileRequest
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
@SpringBootTest
@AutoConfigureWebMvc
class UserProfileIntegrationTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Autowired
private lateinit var objectMapper: ObjectMapper
@Test
fun `should create and retrieve user profile`() {
// Create user profile
val createRequest = CreateUserProfileRequest(
name = "John Doe",
email = "john.doe@example.com",
bio = "Software Developer"
)
val createResult = mockMvc.perform(
post("/api/v1/user-profiles")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createRequest))
)
.andExpect(status().isCreated)
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("john.doe@example.com"))
.andExpect(jsonPath("$.bio").value("Software Developer"))
.andExpect(jsonPath("$.isActive").value(true))
.andReturn()
// Extract ID from response
val responseContent = createResult.response.contentAsString
val createdProfile = objectMapper.readTree(responseContent)
val profileId = createdProfile.get("id").asText()
// Retrieve user profile
mockMvc.perform(get("/api/v1/user-profiles/$profileId"))
.andExpect(status().isOk)
.andExpected(jsonPath("$.id").value(profileId))
.andExpected(jsonPath("$.name").value("John Doe"))
}
@Test
fun `should update user profile`() {
// First create a profile
val createRequest = CreateUserProfileRequest(
name = "Jane Doe",
email = "jane.doe@example.com",
bio = "Designer"
)
val createResult = mockMvc.perform(
post("/api/v1/user-profiles")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createRequest))
).andReturn()
val profileId = objectMapper.readTree(createResult.response.contentAsString)
.get("id").asText()
// Update the profile
val updateRequest = UpdateUserProfileRequest(
name = "Jane Smith",
email = "jane.smith@example.com",
bio = "Senior Designer"
)
mockMvc.perform(
put("/api/v1/user-profiles/$profileId")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateRequest))
)
.andExpected(status().isOk)
.andExpected(jsonPath("$.name").value("Jane Smith"))
.andExpected(jsonPath("$.email").value("jane.smith@example.com"))
.andExpected(jsonPath("$.bio").value("Senior Designer"))
}
@Test
fun `should return 404 for non-existent profile`() {
mockMvc.perform(get("/api/v1/user-profiles/non-existent-id"))
.andExpected(status().isNotFound)
}
}
๐ Phase 5: Test Your Implementationโ
Run Testsโ
# Run all tests
nx test user-profile
# Run specific test classes
nx test user-profile --tests "*UserProfileTest"
nx test user-profile --tests "*IntegrationTest"
Start the Serviceโ
# Start infrastructure
cd infra/docker-compose && docker compose up -d && cd ../..
# Start your service
nx run user-profile:run
Test with curlโ
# Create a user profile
curl -X POST http://localhost:8080/api/v1/user-profiles \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john.doe@example.com"
}'
# Get the profile (replace {id} with actual ID from create response)
curl http://localhost:8080/api/v1/user-profiles/{id}
# Update the profile
curl -X PUT http://localhost:8080/api/v1/user-profiles/{id} \
-H "Content-Type: application/json" \
-d '{
"name": "John Smith",
"email": "john.smith@example.com"
}'
# Get all profiles
curl http://localhost:8080/api/v1/user-profiles
# Deactivate profile
curl -X DELETE http://localhost:8080/api/v1/user-profiles/{id}
Verify Event Publishingโ
Check NATS monitoring UI
# Open NATS monitoring UI
open http://localhost:8222
๐ฏ What You've Accomplishedโ
Congratulations! You've successfully implemented:
โ Domain-Driven Designโ
- Rich domain model with business rules
- Clear command and event definitions
- Proper aggregate boundaries
โ Hexagonal Architectureโ
- Domain isolated from infrastructure
- Input and output ports clearly defined
- Dependency inversion principle applied
โ Test-Driven Developmentโ
- Comprehensive test coverage
- Tests written before implementation
- Both unit and integration tests
โ CQRS/ES Patternsโ
- Command-based operations
- Event publishing for state changes
- Proper separation of concerns
โ EAF Integrationโ
- NATS event publishing
- Spring Boot configuration
- Proper error handling
๐ Next Stepsโ
Now that you have a working backend service with event publishing, let's create a frontend to interact with it! Continue to Frontend Integration to build a React component that consumes your API.
Excellent work! You've built a complete, production-ready service following EAF best practices. The foundation is solid - now let's add the user interface! ๐