Skip to main content

Domain Architecture Builder

A specialized architecture generator focused on creating cohesive domain boundaries following Domain-Driven Design principles, generating domain services with GraphQL gateways for clean API abstraction.

Overview

This archetype creates a domain-centric architecture that emphasizes bounded contexts, domain modeling, and clean separation of concerns. It generates a focused set of components including domain GraphQL gateways and supporting microservices designed around specific business domains.

Technology Stack

  • Java Spring Boot: Domain services and business logic
  • GraphQL: Domain-specific API gateways
  • gRPC: Inter-service communication
  • PostgreSQL: Domain data persistence
  • Redis: Caching and session management
  • Docker: Containerization
  • Kubernetes: Orchestration

Key Concepts

Domain-Driven Design (DDD)

  • Bounded Context: Clear domain boundaries
  • Ubiquitous Language: Consistent terminology within domain
  • Aggregate Root: Consistency and transaction boundaries
  • Domain Events: Decoupled domain communication
  • Value Objects: Immutable domain concepts

Architecture Patterns

  • Hexagonal Architecture: Ports and adapters pattern
  • CQRS: Command Query Responsibility Segregation
  • Event Sourcing: Domain event persistence
  • Saga Pattern: Distributed transaction management

Generated Architecture

Core Components

1. Domain GraphQL Gateway

A specialized GraphQL gateway that provides:

  • Domain-Specific Schema: GraphQL schema tailored to domain concepts
  • Business Logic Coordination: Orchestrates calls to domain services
  • Data Transformation: Maps between GraphQL and gRPC representations
  • Security: Authentication and authorization at domain level
  • Caching: Intelligent caching of domain data
# Example Domain Schema
type User @key(fields: "id") {
id: ID!
username: String!
email: String!
profile: UserProfile
preferences: UserPreferences
activities: [UserActivity!]!
}

type UserProfile {
firstName: String!
lastName: String!
avatar: String
biography: String
}

type UserPreferences {
language: Language!
timezone: String!
notifications: NotificationSettings!
}

type Query {
user(id: ID!): User
users(filter: UserFilter): [User!]!
userActivity(userId: ID!, period: TimePeriod!): [UserActivity!]!
}

type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
updateUserPreferences(userId: ID!, preferences: UserPreferencesInput!): UserPreferences!
}

2. Domain Service

A microservice implementing domain business logic:

// Aggregate Root
@Entity
public class User implements AggregateRoot<UserId> {

@EmbeddedId
private UserId id;

@Embedded
private Username username;

@Embedded
private Email email;

@Embedded
private UserProfile profile;

@Embedded
private UserPreferences preferences;

private List<DomainEvent> domainEvents = new ArrayList<>();

public void updateProfile(UserProfile newProfile) {
this.profile = newProfile;
this.domainEvents.add(new UserProfileUpdatedEvent(this.id, newProfile));
}

public void changeEmail(Email newEmail) {
Email oldEmail = this.email;
this.email = newEmail;
this.domainEvents.add(new UserEmailChangedEvent(this.id, oldEmail, newEmail));
}

@Override
public List<DomainEvent> getDomainEvents() {
return Collections.unmodifiableList(domainEvents);
}

@Override
public void clearDomainEvents() {
domainEvents.clear();
}
}

// Value Objects
@Embeddable
public class Username {
private String value;

protected Username() {} // JPA

public Username(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Username cannot be empty");
}
if (value.length() < 3 || value.length() > 50) {
throw new IllegalArgumentException("Username must be between 3 and 50 characters");
}
this.value = value.trim();
}

public String getValue() {
return value;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Username username = (Username) o;
return Objects.equals(value, username.value);
}

@Override
public int hashCode() {
return Objects.hash(value);
}
}

// Domain Service
@Service
public class UserDomainService {

private final UserRepository userRepository;
private final DomainEventPublisher eventPublisher;

public UserDomainService(UserRepository userRepository, DomainEventPublisher eventPublisher) {
this.userRepository = userRepository;
this.eventPublisher = eventPublisher;
}

public boolean isUsernameAvailable(Username username) {
return !userRepository.existsByUsername(username);
}

public void validateUserCreation(CreateUserCommand command) {
if (!isUsernameAvailable(command.getUsername())) {
throw new UsernameAlreadyExistsException(command.getUsername());
}

if (!isEmailAvailable(command.getEmail())) {
throw new EmailAlreadyExistsException(command.getEmail());
}
}

public User createUser(CreateUserCommand command) {
validateUserCreation(command);

User user = new User(
userRepository.nextId(),
command.getUsername(),
command.getEmail(),
command.getProfile()
);

User savedUser = userRepository.save(user);
eventPublisher.publishEvents(savedUser.getDomainEvents());
savedUser.clearDomainEvents();

return savedUser;
}
}

Project Structure

domain-architecture/
├── domain-gateway/ # GraphQL Domain Gateway
│ ├── src/main/java/
│ │ ├── config/ # Configuration classes
│ │ ├── graphql/ # GraphQL resolvers and types
│ │ │ ├── resolvers/
│ │ │ ├── types/
│ │ │ └── scalars/
│ │ ├── grpc/ # gRPC client configuration
│ │ ├── security/ # Security configuration
│ │ └── DomainGatewayApplication.java
│ ├── src/main/resources/
│ │ ├── graphql/ # GraphQL schema files
│ │ │ ├── schema.graphqls
│ │ │ ├── user.graphqls
│ │ │ └── mutations.graphqls
│ │ └── application.yml
│ ├── Dockerfile
│ └── pom.xml
├── domain-service/ # Core Domain Service
│ ├── src/main/java/
│ │ ├── domain/ # Domain layer
│ │ │ ├── model/ # Aggregates, entities, value objects
│ │ │ ├── service/ # Domain services
│ │ │ ├── event/ # Domain events
│ │ │ └── repository/ # Repository interfaces
│ │ ├── application/ # Application layer
│ │ │ ├── command/ # Command handlers
│ │ │ ├── query/ # Query handlers
│ │ │ └── service/ # Application services
│ │ ├── infrastructure/ # Infrastructure layer
│ │ │ ├── persistence/ # JPA repositories
│ │ │ ├── messaging/ # Event publishing
│ │ │ └── grpc/ # gRPC service implementation
│ │ └── DomainServiceApplication.java
│ ├── src/main/resources/
│ │ ├── db/migration/ # Flyway migrations
│ │ ├── proto/ # Protocol buffer definitions
│ │ └── application.yml
│ ├── Dockerfile
│ └── pom.xml
├── shared/ # Shared libraries
│ ├── domain-common/ # Common domain concepts
│ ├── messaging/ # Event messaging
│ └── security/ # Security utilities
├── infrastructure/
│ ├── docker-compose.yml # Local development
│ ├── kubernetes/ # K8s manifests
│ │ ├── gateway/
│ │ ├── service/
│ │ └── database/
│ └── monitoring/ # Observability
├── docs/ # Architecture documentation
├── scripts/ # Build and deployment scripts
└── README.md

Command and Query Handling

Command Handling (CQRS)

// Command
public class CreateUserCommand {
private final Username username;
private final Email email;
private final UserProfile profile;

public CreateUserCommand(Username username, Email email, UserProfile profile) {
this.username = username;
this.email = email;
this.profile = profile;
}

// Getters...
}

// Command Handler
@Component
public class CreateUserCommandHandler implements CommandHandler<CreateUserCommand, User> {

private final UserDomainService userDomainService;
private final ApplicationEventPublisher eventPublisher;

public CreateUserCommandHandler(UserDomainService userDomainService,
ApplicationEventPublisher eventPublisher) {
this.userDomainService = userDomainService;
this.eventPublisher = eventPublisher;
}

@Transactional
@Override
public User handle(CreateUserCommand command) {
User user = userDomainService.createUser(command);

// Publish integration events
eventPublisher.publishEvent(new UserCreatedIntegrationEvent(
user.getId(),
user.getUsername(),
user.getEmail()
));

return user;
}
}

Query Handling

// Query
public class GetUserQuery {
private final UserId userId;

public GetUserQuery(UserId userId) {
this.userId = userId;
}

public UserId getUserId() {
return userId;
}
}

// Query Handler
@Component
public class GetUserQueryHandler implements QueryHandler<GetUserQuery, UserDto> {

private final UserReadModelRepository userReadModelRepository;

public GetUserQueryHandler(UserReadModelRepository userReadModelRepository) {
this.userReadModelRepository = userReadModelRepository;
}

@Override
public UserDto handle(GetUserQuery query) {
UserReadModel userReadModel = userReadModelRepository.findById(query.getUserId())
.orElseThrow(() -> new UserNotFoundException(query.getUserId()));

return new UserDto(
userReadModel.getId(),
userReadModel.getUsername(),
userReadModel.getEmail(),
userReadModel.getProfile(),
userReadModel.getPreferences()
);
}
}

Event-Driven Architecture

Domain Events

// Domain Event
public class UserEmailChangedEvent implements DomainEvent {
private final UserId userId;
private final Email oldEmail;
private final Email newEmail;
private final Instant occurredAt;

public UserEmailChangedEvent(UserId userId, Email oldEmail, Email newEmail) {
this.userId = userId;
this.oldEmail = oldEmail;
this.newEmail = newEmail;
this.occurredAt = Instant.now();
}

// Getters...
}

// Event Handler
@Component
public class UserEmailChangedEventHandler {

private final EmailVerificationService emailVerificationService;
private final UserNotificationService notificationService;

@EventHandler
public void handle(UserEmailChangedEvent event) {
// Send email verification
emailVerificationService.sendVerificationEmail(event.getNewEmail());

// Notify user of email change
notificationService.sendEmailChangeNotification(
event.getUserId(),
event.getOldEmail()
);
}
}

Integration Events

// Integration Event
public class UserCreatedIntegrationEvent {
private final String userId;
private final String username;
private final String email;
private final Instant timestamp;

public UserCreatedIntegrationEvent(UserId userId, Username username, Email email) {
this.userId = userId.getValue();
this.username = username.getValue();
this.email = email.getValue();
this.timestamp = Instant.now();
}

// Getters...
}

// Event Publisher
@Service
public class IntegrationEventPublisher {

private final KafkaTemplate<String, Object> kafkaTemplate;

@EventListener
public void handleUserCreated(UserCreatedIntegrationEvent event) {
kafkaTemplate.send("user-events", event.getUserId(), event);
}
}

GraphQL Gateway Implementation

Resolvers

@Component
public class UserResolver implements GraphQLQueryResolver, GraphQLMutationResolver {

private final UserServiceGrpcClient userServiceClient;

public UserResolver(UserServiceGrpcClient userServiceClient) {
this.userServiceClient = userServiceClient;
}

// Query resolvers
public UserDto user(String id) {
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId(id)
.build();

GetUserResponse response = userServiceClient.getUser(request);
return mapToUserDto(response.getUser());
}

public List<UserDto> users(UserFilterInput filter) {
GetUsersRequest request = GetUsersRequest.newBuilder()
.setFilter(mapToUserFilter(filter))
.build();

GetUsersResponse response = userServiceClient.getUsers(request);
return response.getUsersList().stream()
.map(this::mapToUserDto)
.collect(Collectors.toList());
}

// Mutation resolvers
public UserDto createUser(CreateUserInput input) {
CreateUserRequest request = CreateUserRequest.newBuilder()
.setUsername(input.getUsername())
.setEmail(input.getEmail())
.setProfile(mapToUserProfile(input.getProfile()))
.build();

CreateUserResponse response = userServiceClient.createUser(request);
return mapToUserDto(response.getUser());
}

// Field resolvers
public CompletableFuture<List<UserActivityDto>> activities(UserDto user, DataLoader<String, List<UserActivityDto>> activityLoader) {
return activityLoader.load(user.getId());
}

private UserDto mapToUserDto(com.example.grpc.User grpcUser) {
return UserDto.builder()
.id(grpcUser.getId())
.username(grpcUser.getUsername())
.email(grpcUser.getEmail())
.profile(mapToUserProfileDto(grpcUser.getProfile()))
.preferences(mapToUserPreferencesDto(grpcUser.getPreferences()))
.build();
}
}

DataLoader for N+1 Prevention

@Component
public class UserDataLoaderRegistry {

private final UserServiceGrpcClient userServiceClient;

@Bean
public DataLoader<String, List<UserActivityDto>> userActivityDataLoader() {
return DataLoader.newMappedDataLoader(userIds -> {
GetUserActivitiesRequest request = GetUserActivitiesRequest.newBuilder()
.addAllUserIds(userIds)
.build();

GetUserActivitiesResponse response = userServiceClient.getUserActivities(request);

return userIds.stream()
.collect(Collectors.toMap(
userId -> userId,
userId -> response.getActivitiesMap().getOrDefault(userId,
UserActivitiesList.getDefaultInstance()).getActivitiesList()
.stream()
.map(this::mapToUserActivityDto)
.collect(Collectors.toList())
));
});
}
}

Testing Strategy

Domain Testing

@Test
class UserTest {

@Test
void shouldUpdateProfile() {
// Given
User user = new User(
new UserId("user-123"),
new Username("johndoe"),
new Email("john@example.com"),
UserProfile.builder()
.firstName("John")
.lastName("Doe")
.build()
);

UserProfile newProfile = UserProfile.builder()
.firstName("Johnny")
.lastName("Doe")
.biography("Software developer")
.build();

// When
user.updateProfile(newProfile);

// Then
assertThat(user.getProfile()).isEqualTo(newProfile);
assertThat(user.getDomainEvents()).hasSize(1);
assertThat(user.getDomainEvents().get(0)).isInstanceOf(UserProfileUpdatedEvent.class);
}

@Test
void shouldThrowExceptionForInvalidUsername() {
assertThatThrownBy(() -> new Username("ab"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Username must be between 3 and 50 characters");
}
}

Application Layer Testing

@ExtendWith(MockitoExtension.class)
class CreateUserCommandHandlerTest {

@Mock
private UserDomainService userDomainService;

@Mock
private ApplicationEventPublisher eventPublisher;

@InjectMocks
private CreateUserCommandHandler handler;

@Test
void shouldCreateUser() {
// Given
CreateUserCommand command = new CreateUserCommand(
new Username("johndoe"),
new Email("john@example.com"),
UserProfile.builder().firstName("John").lastName("Doe").build()
);

User expectedUser = new User(
new UserId("user-123"),
command.getUsername(),
command.getEmail(),
command.getProfile()
);

when(userDomainService.createUser(command)).thenReturn(expectedUser);

// When
User result = handler.handle(command);

// Then
assertThat(result).isEqualTo(expectedUser);
verify(eventPublisher).publishEvent(any(UserCreatedIntegrationEvent.class));
}
}

GraphQL Integration Testing

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserResolverIntegrationTest {

@Autowired
private TestRestTemplate restTemplate;

@MockBean
private UserServiceGrpcClient userServiceClient;

@Test
void shouldCreateUser() throws Exception {
// Given
String mutation = """
mutation {
createUser(input: {
username: "johndoe"
email: "john@example.com"
profile: {
firstName: "John"
lastName: "Doe"
}
}) {
id
username
email
profile {
firstName
lastName
}
}
}
""";

CreateUserResponse grpcResponse = CreateUserResponse.newBuilder()
.setUser(com.example.grpc.User.newBuilder()
.setId("user-123")
.setUsername("johndoe")
.setEmail("john@example.com")
.build())
.build();

when(userServiceClient.createUser(any())).thenReturn(grpcResponse);

// When
GraphQLRequest request = new GraphQLRequest(mutation);
ResponseEntity<GraphQLResponse> response = restTemplate.postForEntity(
"/graphql", request, GraphQLResponse.class);

// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getData()).isNotNull();
}
}

Deployment Configuration

Docker Compose (Local Development)

version: '3.8'

services:
domain-gateway:
build: ./domain-gateway
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=local
- DOMAIN_SERVICE_URL=domain-service:9090
depends_on:
- domain-service
- redis

domain-service:
build: ./domain-service
ports:
- "9090:9090"
environment:
- SPRING_PROFILES_ACTIVE=local
- DATABASE_URL=postgresql://postgres:password@postgres:5432/domaindb
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- redis

postgres:
image: postgres:14
environment:
POSTGRES_DB: domaindb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data

redis:
image: redis:7-alpine
ports:
- "6379:6379"

volumes:
postgres_data:

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: domain-gateway
spec:
replicas: 3
selector:
matchLabels:
app: domain-gateway
template:
metadata:
labels:
app: domain-gateway
spec:
containers:
- name: gateway
image: your-registry/domain-gateway:latest
ports:
- containerPort: 8080
env:
- name: DOMAIN_SERVICE_URL
value: "domain-service:9090"
- name: REDIS_URL
value: "redis://redis:6379"
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 500m
memory: 1Gi
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5

Quick Start

  1. Generate the domain architecture:

    archetect render git@github.com:p6m-archetypes/domain-architecture-builder.archetype.git
  2. Start local development environment:

    docker-compose up -d
  3. Build and run services:

    # Build all services
    ./scripts/build.sh

    # Run domain service
    cd domain-service
    ./gradlew bootRun

    # Run domain gateway (in another terminal)
    cd domain-gateway
    ./gradlew bootRun
  4. Access GraphQL Playground:

    http://localhost:8080/playground
  5. Test the API:

    mutation {
    createUser(input: {
    username: "johndoe"
    email: "john@example.com"
    profile: {
    firstName: "John"
    lastName: "Doe"
    }
    }) {
    id
    username
    email
    profile {
    firstName
    lastName
    }
    }
    }

Best Practices

Domain Modeling

  • Focus on business concepts and ubiquitous language
  • Keep aggregates small and focused
  • Use value objects for domain concepts
  • Implement proper domain validation

Architecture

  • Maintain clear boundaries between layers
  • Use dependency injection appropriately
  • Implement proper error handling
  • Design for testability

Performance

  • Use read models for queries
  • Implement caching strategies
  • Optimize database queries
  • Use connection pooling

Security

  • Implement proper authentication and authorization
  • Validate all inputs
  • Use HTTPS for all communications
  • Implement audit logging