Skip to content
Merged
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
243 changes: 199 additions & 44 deletions CLAUDE.md

Large diffs are not rendered by default.

106 changes: 69 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# User Service - Java/Spring Boot

A comprehensive user authentication and management service built with Java 21 and Spring Boot 3.
A comprehensive user authentication and management service built with **Java 25** and **Spring Boot 4**, architected as a **modular monolith** using **Spring Modulith**.

## Features

Expand All @@ -27,21 +27,23 @@ A comprehensive user authentication and management service built with Java 21 an

## Tech Stack

- **Java 25** (Latest LTS)
- **Spring Boot 4**
- **Spring Security 6**
- **Java 25** with Virtual Threads (Project Loom)
- **Spring Boot 4.0** with Spring Framework 7
- **Spring Modulith 2.0** for modular architecture
- **Spring Security 7**
- **Spring Data JPA**
- **PostgreSQL 17**
- **JWT (JJWT)**
- **gRPC** alongside REST APIs
- **JWT (JJWT 0.13)**
- **Gradle 8+**
- **Docker & Docker Compose**
- **Swagger/OpenAPI 3**
- **Swagger/OpenAPI 3** (springdoc-openapi)

## Quick Start

### Prerequisites

- Java 21
- Java 25
- Gradle 8+
- PostgreSQL 17+
- Docker & Docker Compose (optional)
Expand Down Expand Up @@ -223,43 +225,70 @@ curl -X POST http://localhost:3001/api/users/auth/refresh \
### Code Quality

```bash
# Check code style
./gradlew checkstyleMain checkstyleTest
# Apply code formatting (Palantir Java Format)
./gradlew spotlessApply

# Check code formatting
./gradlew spotlessCheck

# Verify module architecture
./gradlew test --tests "org.nkcoder.ModulithArchitectureTest"

# Generate dependency report
./gradlew dependencyInsight --dependency <dependency-name>
```

## Project Structure

The application uses **Spring Modulith** for a modular monolith architecture:

```
src/main/java/org/nkcoder/
├── Application.java # Bootstrap (@Modulith entry point)
├── user/ # User Module
│ ├── package-info.java # @ApplicationModule definition
│ ├── interfaces/rest/ # REST controllers
│ │ ├── AuthController.java
│ │ ├── UserController.java
│ │ └── AdminUserController.java
│ ├── application/service/ # Application services
│ │ ├── AuthApplicationService.java
│ │ └── UserApplicationService.java
│ ├── domain/ # Domain layer
│ │ ├── model/ # Aggregates, entities, value objects
│ │ ├── service/ # Domain services (ports)
│ │ └── repository/ # Repository interfaces (ports)
│ └── infrastructure/ # Infrastructure layer
│ ├── persistence/ # JPA entities & repository adapters
│ └── security/ # JWT filter, SecurityConfig
├── notification/ # Notification Module
│ ├── package-info.java
│ ├── NotificationService.java # Public API
│ └── application/ # Event listeners
├── shared/ # Shared Kernel (OPEN module)
│ ├── kernel/
│ │ ├── domain/event/ # Domain events
│ │ └── exception/ # Common exceptions
│ └── local/rest/ # ApiResponse, GlobalExceptionHandler
└── infrastructure/ # Infrastructure Module (OPEN module)
└── config/ # CORS, OpenAPI, JPA auditing
```

### Module Dependencies

```
user-service/
├── src/
│ ├── main/
│ │ ├── java/com/timor/user/
│ │ │ ├── config/ # Configuration classes
│ │ │ ├── controller/ # REST controllers
│ │ │ ├── dto/ # Data Transfer Objects
│ │ │ ├── entity/ # JPA entities
│ │ │ ├── enums/ # Enums
│ │ │ ├── exception/ # Custom exceptions
│ │ │ ├── mapper/ # Entity-DTO mappers
│ │ │ ├── repository/ # Data repositories
│ │ │ ├── security/ # Security components
│ │ │ ├── service/ # Business logic
│ │ │ └── util/ # Utility classes
│ │ └── resources/
│ │ ├── application.yml
│ │ └── application-docker.yml
│ └── test/ # Test files
├── scripts/
│ └── init.sql # Database initialization
├── docker-compose.yml
├── Dockerfile
├── build.gradle
└── README.md
notification ──→ shared ←── user
infrastructure
```

- Modules communicate via **domain events** (not direct calls)
- `shared` and `infrastructure` are OPEN modules (accessible by all)

## Security Features

- **JWT Authentication**: Stateless authentication with HS512 algorithm
Expand All @@ -281,9 +310,10 @@ user-service/

### Application Profiles

- `default`: Local development
- `docker`: Docker container environment
- `test`: Testing environment
- `local`: Local development with Docker Compose for PostgreSQL
- `dev`: Development environment with external database
- `prod`: Production environment
- `test`: Testing with TestContainers

### Key Configuration Properties

Expand All @@ -307,6 +337,8 @@ spring:

## References

- [Spring Modulith Documentation](https://docs.spring.io/spring-modulith/reference/)
- [Spring Boot 4.0 Migration Guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide)
- [Implementing Domain Driven Design with Spring](https://github.com/maciejwalkowiak/implementing-ddd-with-spring-talk)

## License
Expand Down
2 changes: 1 addition & 1 deletion auto/docker_logs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env sh

docker compose -f docker-compose-all.yml up -
docker compose -f docker-compose-all.yml logs -f --tail 100
2 changes: 1 addition & 1 deletion auto/run
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env sh

./gradlew bootRun --args='--spring.profiles.active=local' --no-daemon
./gradlew bootRun --args='--spring.profiles.active=local' --no-daemon
8 changes: 7 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ plugins {
id("org.springframework.boot") version "4.0.0"
id("io.spring.dependency-management") version "1.1.7"
id("org.graalvm.buildtools.native") version "0.11.1"
id("org.jetbrains.kotlin.jvm") version "2.2.21"
id("org.flywaydb.flyway") version "11.11.1"
id("com.diffplug.spotless") version "8.1.0"
id("com.google.protobuf") version "0.9.5"
java
jacoco
kotlin("jvm") version "2.2.21"
}

group = "org.nkcoder"
Expand All @@ -27,6 +27,7 @@ repositories {

extra["testcontainersVersion"] = "1.21.3"
extra["jjwtVersion"] = "0.13.0"
extra["springModulithVersion"] = "2.0.0"

dependencies {
// Spring Boot Starters
Expand All @@ -37,6 +38,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-jackson")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0")
implementation("org.springframework.modulith:spring-modulith-starter-core")
implementation("org.springframework.modulith:spring-modulith-starter-jpa")

// Database
implementation("org.springframework.boot:spring-boot-starter-flyway")
Expand All @@ -62,6 +65,7 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.springframework.modulith:spring-modulith-starter-test")

// gRPC and Protobuf
implementation("io.grpc:grpc-netty-shaded:1.77.0")
Expand All @@ -70,11 +74,13 @@ dependencies {
implementation("com.google.protobuf:protobuf-java:4.33.1")
implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")
implementation("org.springframework.grpc:spring-grpc-spring-boot-starter:1.0.0")
implementation(kotlin("stdlib-jdk8"))
}

dependencyManagement {
imports {
mavenBom("org.testcontainers:testcontainers-bom:${property("testcontainersVersion")}")
mavenBom("org.springframework.modulith:spring-modulith-bom:${property("springModulithVersion")}")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.modulith.Modulith;

@Modulith(
systemName = "Application",
sharedModules = {"shared", "infrastructure"})
@SpringBootApplication
@ConfigurationPropertiesScan
public class UserApplication {

public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
public class Application {
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/nkcoder/infrastructure/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Infrastructure module containing cross-cutting concerns.
*
* <p>This module provides:
*
* <ul>
* <li>Security configuration (JWT, CORS)
* <li>Web configuration
* <li>OpenAPI/Swagger configuration
* <li>JPA auditing configuration
* </ul>
*
* <p>This is a shared module - all other modules can access it.
*/
@ApplicationModule(type = ApplicationModule.Type.OPEN)
package org.nkcoder.infrastructure;

import org.springframework.modulith.ApplicationModule;
16 changes: 16 additions & 0 deletions src/main/java/org/nkcoder/notification/NotificationService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.nkcoder.notification;

import org.springframework.stereotype.Service;

@Service
public class NotificationService {
public void sendWelcomeEmail(String email, String userName) {
// TODO: implement email sending
System.out.println("Sending Welcome email to " + email + ", for user: " + userName);
}

public void sendPasswordResetEmail(String email, String userName) {
// TODO: implement password reset email
System.out.println("Sending password reset email to " + email + ", for user: " + userName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.nkcoder.notification.application;

import org.nkcoder.notification.NotificationService;
import org.nkcoder.shared.kernel.domain.event.UserRegisteredEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;

@Component
public class UserEventListener {
private final NotificationService notificationService;

public UserEventListener(NotificationService notificationService) {
this.notificationService = notificationService;
}

@ApplicationModuleListener
public void onUserRegistered(UserRegisteredEvent event) {
notificationService.sendWelcomeEmail(event.email(), event.userName());
}
}
20 changes: 20 additions & 0 deletions src/main/java/org/nkcoder/notification/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* The Notification module handles email and SMS notifications.
*
* <p>This module provides:
*
* <ul>
* <li>Email notifications
* <li>SMS notifications (future)
* </ul>
*
* <p>Listens to events:
*
* <ul>
* <li>{@code UserRegisteredEvent} - sends welcome email
* </ul>
*/
@ApplicationModule(allowedDependencies = {"shared", "infrastructure"})
package org.nkcoder.notification;

import org.springframework.modulith.ApplicationModule;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.nkcoder.shared.kernel.domain.event;

import java.time.LocalDateTime;
import java.util.UUID;

public record UserRegisteredEvent(UUID userId, String email, String userName, LocalDateTime occurredOn)
implements DomainEvent {
public UserRegisteredEvent(UUID userId, String email, String userName) {
this(userId, email, userName, LocalDateTime.now());
}

@Override
public String eventType() {
return "user.registered";
}

@Override
public LocalDateTime occurredOn() {
return occurredOn;
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/nkcoder/shared/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Shared kernel containing domain-driven design fundamentals.
*
* <p>This module provides:
*
* <ul>
* <li>Base domain event infrastructure
* <li>Aggregate root base class
* <li>Common exceptions
* <li>REST utilities (ApiResponse, GlobalExceptionHandler)
* </ul>
*
* <p>This is a shared module - all other modules can access it.
*/
@ApplicationModule(type = ApplicationModule.Type.OPEN)
package org.nkcoder.shared;

import org.springframework.modulith.ApplicationModule;
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package org.nkcoder.infrastructure.resolver;
package org.nkcoder.user.infrastructure;

import jakarta.servlet.http.HttpServletRequest;
import java.util.UUID;
import org.jetbrains.annotations.NotNull;
import org.nkcoder.shared.local.annotation.CurrentUser;
import org.nkcoder.user.infrastructure.security.JwtAuthenticationFilter;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
Expand All @@ -13,7 +14,7 @@

/**
* Resolves method parameters annotated with {@link CurrentUser} by extracting the user ID from request attributes set
* by {@link org.nkcoder.infrastructure.security.JwtAuthenticationFilter}
* by {@link JwtAuthenticationFilter}
*/
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.nkcoder.infrastructure.config;
package org.nkcoder.user.infrastructure;

import java.util.List;
import org.nkcoder.infrastructure.resolver.CurrentUserArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.nkcoder.infrastructure.security;
package org.nkcoder.user.infrastructure.security;

import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down
Loading
Loading