
Ddd Devops Integration
- 14 installs
- 1 repo stars
- Updated July 29, 2026
- full-statck-skills/ddd-skills
Integrates DDD projects into CI/CD with ArchUnit quality gates, multi-module builds, architecture-aware Dockerfiles, and domain-event health monitoring.
About
Guides DevOps for DDD projects including ArchUnit P0/P1/P2 quality gates in CI/CD, containerized deployment, and domain-event monitoring. A developer uses it to automate builds, architecture validation, and deployment of DDD projects.
- ArchUnit quality gates block merges, not deploys
- K8s deploy with per-bounded-context namespaces
Ddd Devops Integration by the numbers
- 14 all-time installs (skills.sh)
- Ranked #957 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/full-statck-skills/ddd-skills --skill ddd-devops-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | full-statck-skills/ddd-skills ↗ |
What it does
Integrates DDD projects into CI/CD with ArchUnit quality gates, multi-module builds, architecture-aware Dockerfiles, and domain-event health monitoring.
Files
DDD DevOps Integration
DevOps for DDD projects — ArchUnit quality gates (P0 block / P1 warn / P2 report) in CI/CD, multi-module incremental builds, architecture-type-aware Dockerfiles, event-driven DB migration, domain event health monitoring.
---
Workflow
Step 1: Code & Build — Compile all modules. Domain must compile last (zero framework contamination).
Step 2: Unit Test — Per-module unit tests run independently.
Step 3: ArchUnit Validate — P0 (zero deps, layer compliance) blocks CI; P1 (cycle-free) warns; P2 (naming) reports.
Step 4: Integration Test — Cross-module interaction tests with event store and outbox.
Step 5: Containerize — Build Docker images (monolith fat JAR or split CQRS with tuned JVM args).
Step 6: Deploy — K8s deploy with per-BC namespace, DB migration init containers, health probes.
Step 7: Monitor — Domain metrics (event rate, outbox depth, aggregate load) → Prometheus alerts.
Quality gates run at build time, never against production. Violations block merge, not deployment.
---
Boundary
✅ 擅长处理
- 为 DDD 项目配置 ArchUnit 质量门禁(P0/P1/P2),集成到 CI/CD Pipeline
- 多模块 DDD 项目增量编译优化(Maven/Gradle)、并行构建加速
- 架构类型感知的 Dockerfile 生成(单体/CQRS/六边形架构)
- K8s 部署模板:Per-BC Namespace、DB 迁移 initContainers、领域健康探针
- 事件驱动数据库迁移:Event Store、Outbox 表创建与索引策略
- 领域事件健康监控:Prometheus 指标暴露与告警规则配置
⚠️ 需要素材
- 项目结构:Maven/Gradle 模块列表、依赖关系图 → 自动生成 ArchUnit 规则
- 部署环境:K8s 集群版本、Ingress 策略 → 生成适配的 YAML 模板
- 数据库方案:采用 Layered/CQRS/Event Sourcing 何种模式 → 定制迁移策略
❌ 超出范围(不适用)
- 通用 CI/CD 配置(非 DDD 项目不适用)→ 使用通用 DevOps 工具
- 容器编排基础(K8s 入门教程)→ 参考官方文档
- K8s 集群运维(节点管理、RBAC 配置)→ 使用专业运维工具
- 通用数据库管理(备份、恢复、性能调优)→ 使用 DBA 工具
- 基础设施监控(CPU、内存、网络)→ 使用 Prometheus + Grafana
---
Audience
This skill is designed for: Backend developers (implementing DDD architectures), Software architects (evaluating and selecting patterns), Tech leads (reviewing team implementations), and DDD beginners (learning domain-driven design fundamentals).
Rules
1. ArchUnit P0 checks (domain purity, layer compliance) must block CI pipeline on failure. 2. Dockerfiles must be architecture-type-aware (Monolith/CQRS/Hexagonal each get different builds). 3. Domain event monitoring must be configured before production deployment. 4. Database migrations for event-driven systems must include event store and outbox tables.
---
CI/CD Pipeline
stages:
- build
- unit-test
- architecture-check # DDD gate
- integration-test
- containerize
- deployArchUnit validation in architecture-check stage: P0 tests (DomainPurityTest, LayeringComplianceTest) block on failure; P1 (ModuleDependencyTest) warns with approval; P2 (NamingConventionTest) reports only. Key ArchUnit rules — domain must not depend on Spring/JPA/MyBatis; layer access restrictions (Domain only by Application, Infrastructure, Interface); no cyclic package dependencies. Full guide: references/ci-cd-archunit-setup.md
---
多模块构建策略
ddd-project/
├── domain/ (zero framework deps) → application/ (depends on domain only) → infrastructure/ (implements domain interfaces)
├── adapter/ (REST/event adapters) → start/ (boot entry)Build optimization: mvn compile -pl domain -am (~60% faster changed-only), mvn -T 4 (~40% parallel), mvn verify -pl '!domain' (skip domain), actions/cache for target/ (~80%). Domain purity enforced via Maven Enforcer (bans Spring deps in domain module). Full config: references/multi-module-build-config.md
---
容器化部署
Monolith — Multi-stage fat JAR:
FROM eclipse-temurin:17-jdk-alpine AS builder
COPY . . && RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre-alpine
COPY --from=builder start/target/*.jar app.jar
HEALTHCHECK --interval=30s CMD curl -f http://localhost:8080/actuator/healthCQRS — Separate images: Command (-Xms512m -Xmx2g -XX:+UseZGC, CPU-optimized); Query (-Xms1g -Xmx4g, memory-optimized). K8s Patterns: Per-BC Namespace, DB migration initContainers + Flyway, CQRS split HorizontalPodAutoscaler, NetworkPolicy isolation. Templates: dockerfile-patterns.md | k8s-ddd-reference.md
---
数据库迁移(事件驱动)
CREATE TABLE domain_event_store (id UUID PRIMARY KEY, aggregate_id VARCHAR(36) NOT NULL,
event_type VARCHAR(200) NOT NULL, event_data JSONB NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL, published BOOLEAN DEFAULT FALSE);
CREATE INDEX idx_events_pending ON domain_event_store(published) WHERE published = FALSE;
CREATE TABLE outbox_message (id UUID PRIMARY KEY, event_id UUID UNIQUE NOT NULL,
payload JSONB NOT NULL, status VARCHAR(20) DEFAULT 'PENDING',
retry_count INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW());
CREATE INDEX idx_outbox_pending ON outbox_message(status) WHERE status = 'PENDING';Strategy: Layered single DB → Flyway/Liquibase sequential; CQRS L2 → Dual tracks + lag; Event Sourcing → Axon/EventStoreDB append-only; Per microservice → Per-BC Flyway. Full scripts: flyway-migration.md | db-strategies.md
---
监控与告警
Bus metrics: domain.events.published (Counter), domain.events.processing.duration (Histogram p50/p95/p99), domain.aggregate.load.duration (Histogram), domain.outbox.depth (Gauge).
Alert rules: DomainEventBacklog (rate > 100 for 2m, critical), OutboxQueueGrowing (depth > 1000 for 1m, critical), EventProcessingErrorRate (> 5% for 2m, warning). Full config: ddd-observability-config.md
---
Gotchas
1. ArchUnit 过严导致开发受阻: 先用 warning 模式运行 2-3 个 sprint,P1/P2 不阻断 CI,团队适应后升级为 error。 2. CQRS 读写容器未分离: 命令服务 CPU 密集,查询服务内存密集,应独立构建部署。 3. 领域事件监控遗漏: Event 投递延迟、Outbox 堆积、Consumer Lag 是事件驱动架构中最易忽略的可靠性指标。 4. 单体打成微服务镜像: 先评估 BC 是否需要独立部署,过度拆分失去聚合内强一致性优势。 5. 数据库迁移未考虑事件存储: 添加 Event Store 表时需同步考虑回填、索引、清理策略。 6. ArchUnit 不检查充血模型: 需结合 ddd-code-reviewer 做领域模型质量审查。
---
FAQ
Q: ArchUnit CI 失败但代码正确? A: 检查 Domain 层是否有框架注解污染(如 @Service),移到基础设施层。 Q: Monolith vs 微服务 DDD 部署策略? A: 从单体开始。仅当 BC 需独立扩缩容、单 BC 团队 > 8 人、BC 间部署节奏差异大时再拆分。 Q: 如何监控领域事件而不增加延迟? A: 异步指标(Micrometer Timer 百分比采样)+ 采样追踪(1/100 OpenTelemetry)。绝不阻塞事件发布。 Q: CQRS 读写应共享同一 pipeline? A: 只共享 Domain 模块测试。Command pipeline 跑完整 ArchUnit 门禁;Query pipeline 聚焦读模型 schema 兼容性。 Q: Event Sourcing schema 如何管理? A: Event Store append-only,不需 schema 迁移。版本兼容性通过事件版本号(event_type + version)管理。 Q: Bounded Context 金丝雀发布如何实现? A: Service Mesh(Istio)流量权重分配。新版本先接 5% 流量,监控 domain event 健康指标后全量。
---
Security & Safety
This skill is pure documentation. It does not collect user data, does not access external services or networks, and contains no executable scripts.
Keywords
ddd devops, ddd ci/cd, archunit ci, domain event monitoring, ddd docker, ddd kubernetes, ddd outbox, ddd database migration, ddd build optimization, ddd quality gate, ddd deployment, ddd alerting, ddd observability, cqrs deployment, event sourcing infrastructure
References
- CI/CD ArchUnit Setup — ArchUnit CI/CD integration guide
- Dockerfile Patterns — Dockerfiles by DDD architecture type
- K8s DDD Reference — K8s deployment for bounded contexts
- Flyway Domain Event Migration — Event store and outbox scripts
- Monitoring & Alerting — Prometheus metrics and alerting rules
- Multi-Module Build Config — Maven/Gradle build optimization
- DDD Observability Config — Micrometer/OTel/Logback configuration
- GitLab CI Pipeline Reference — GitLab CI complete pipeline
- Database Migration Strategies — Event-driven migration strategies
Examples
- GitHub Actions Pipeline — Complete GitHub Actions workflow with ArchUnit
- GitLab CI ArchUnit Pipeline — GitLab CI with architecture validation
- K8s DDD Deployment — K8s manifests for bounded context deployment
- Flyway Domain Event Migration — Complete migration SQL scripts
- Grafana Domain Health Dashboard — Domain event monitoring dashboard
-- ===================================================
-- Flyway Migration: Domain Event Infrastructure
-- For an e-commerce DDD system (Order Bounded Context)
-- ===================================================
-- V1__create_domain_event_store.sql
-- Domain event store for event sourcing and audit
CREATE TABLE domain_event_store (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_id VARCHAR(36) NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
event_type VARCHAR(200) NOT NULL,
event_version INT NOT NULL DEFAULT 1,
event_data JSONB NOT NULL,
metadata JSONB DEFAULT '{}',
occurred_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_event_store_agg ON domain_event_store(aggregate_id, aggregate_type);
CREATE INDEX idx_event_store_type ON domain_event_store(event_type);
CREATE INDEX idx_event_store_time ON domain_event_store(occurred_at DESC);
COMMENT ON TABLE domain_event_store IS 'Domain event store — append-only log of all domain events';
COMMENT ON COLUMN domain_event_store.event_data IS 'Event payload in JSONB format';
COMMENT ON COLUMN domain_event_store.metadata IS 'Correlation ID, causation ID, tenant, etc.';
-- V2__create_outbox_message.sql
-- Outbox pattern for reliable domain event publishing
CREATE TABLE outbox_message (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES domain_event_store(id),
destination VARCHAR(255) NOT NULL,
partition_key VARCHAR(64),
payload JSONB NOT NULL,
headers JSONB DEFAULT '{}',
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
retry_count INT NOT NULL DEFAULT 0,
max_retries INT NOT NULL DEFAULT 3,
last_error TEXT,
scheduled_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
processed_at TIMESTAMPTZ,
UNIQUE(event_id, destination)
);
CREATE INDEX idx_outbox_pending ON outbox_message(status, scheduled_at)
WHERE status = 'PENDING' AND (scheduled_at IS NULL OR scheduled_at <= NOW());
CREATE INDEX idx_outbox_retry ON outbox_message(status, retry_count)
WHERE status = 'RETRYING' AND retry_count < max_retries;
COMMENT ON TABLE outbox_message IS 'Outbox for reliable domain event delivery via message broker';
-- V3__create_order_snapshot.sql
-- Event sourcing snapshots for order aggregates
CREATE TABLE order_snapshot (
aggregate_id VARCHAR(36) PRIMARY KEY,
version INT NOT NULL,
state JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_order_snapshot_version ON order_snapshot(version);
-- V4__create_idempotency_key.sql
-- Idempotency tracking for event consumers
CREATE TABLE idempotency_key (
id VARCHAR(64) PRIMARY KEY,
consumer_id VARCHAR(100) NOT NULL,
event_id UUID NOT NULL,
event_type VARCHAR(200) NOT NULL,
handled_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_idempotency_lookup ON idempotency_key(consumer_id, event_id, event_type);
CREATE INDEX idx_idempotency_cleanup ON idempotency_key(handled_at);
COMMENT ON TABLE idempotency_key IS 'Tracks processed events to ensure exactly-once delivery';
-- V5__seed_initial_config.sql
-- Seed data: domain event type registry
INSERT INTO domain_event_type_registry (event_type, version, schema_version, description)
VALUES
('OrderCreated', 1, 1, 'Order has been created with initial items'),
('OrderItemAdded', 1, 1, 'Item added to existing order'),
('OrderItemRemoved', 1, 1, 'Item removed from order'),
('OrderPaid', 1, 1, 'Order payment completed'),
('OrderShipped', 1, 1, 'Order has been shipped'),
('OrderDelivered', 1, 1, 'Order delivery confirmed'),
('OrderCancelled', 1, 1, 'Order was cancelled')
ON CONFLICT (event_type) DO NOTHING;
-- Create domain event type registry table
CREATE TABLE IF NOT EXISTS domain_event_type_registry (
event_type VARCHAR(200) PRIMARY KEY,
version INT NOT NULL DEFAULT 1,
schema_version INT NOT NULL DEFAULT 1,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ===================================================
-- Rollback Script (V5__rollback.sql)
-- ===================================================
-- DROP TABLE IF EXISTS domain_event_type_registry;
-- DROP TABLE IF EXISTS idempotency_key;
-- DROP TABLE IF EXISTS order_snapshot;
-- DROP TABLE IF EXISTS outbox_message;
-- DROP TABLE IF EXISTS domain_event_store;
GitHub Actions — Full DDD Quality Gate Pipeline
name: DDD Quality Gate
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
env:
MAVEN_OPTS: -Dmaven.repo.local=${{ github.workspace }}/.m2/repository
jobs:
# ──── Build & Unit Tests ────
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build & Unit Tests
run: mvn test -pl domain,application
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: '**/target/surefire-reports/'
# ──── Architecture Validation (Critical Gate) ────
architecture-check:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Domain Purity Check (P0)
run: mvn test -pl domain -Dtest=DomainPurityTest
- name: Layering Compliance Check (P0)
run: mvn test -pl domain -Dtest=LayeringComplianceTest
- name: Module Dependency Check (P1)
run: mvn test -pl domain -Dtest=ModuleDependencyTest
- name: Naming Convention Check (P2)
run: mvn test -pl domain -Dtest=NamingConventionTest
- name: Upload ArchUnit report
if: always()
uses: actions/upload-artifact@v4
with:
name: archunit-report
path: domain/target/surefire-reports/
# ──── Integration Tests ────
integration-test:
needs: architecture-check
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Repository Integration Tests
run: mvn test -pl infrastructure -Dtest='*RepositoryImplTest'
env:
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/testdb
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: test
# ──── Build Docker Image ────
docker-build:
needs: integration-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ secrets.REGISTRY_URL }}/order-service:${{ github.sha }}
${{ secrets.REGISTRY_URL }}/order-service:latest
cache-from: type=gha
cache-to: type=gha,mode=maxFailure Policy Summary
| Check | Severity | CI Behavior | Action Required |
|---|---|---|---|
| Domain purity | P0 | Block merge | Fix domain layer dependencies |
| Layering | P0 | Block merge | Fix cross-layer imports |
| Module cycles | P1 | Warning + approval | Refactor module structure |
| Naming conventions | P2 | Report only | Log for next sprint |
| Integration tests | P0 | Block merge | Fix broken repository tests |
| Docker build | P0 | Block release | Fix Dockerfile or build config |
GitLab CI — ArchUnit Architecture Validation Pipeline
# .gitlab-ci.yml — DDD Architecture Quality Gate
stages:
- build
- architecture-validation
- test
- security
- deploy
variables:
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end"
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .m2/repository/
- domain/target/
# ──── Build Stage ────
compile:
stage: build
script:
- mvn $MAVEN_CLI_OPTS compile -pl domain,application,infrastructure -am
artifacts:
paths:
- domain/target/classes/
- application/target/classes/
- infrastructure/target/classes/
expire_in: 2 hours
# ──── Architecture Validation ────
architecture-validation:
stage: architecture-validation
script:
# P0 Checks — Block on failure
- mvn $MAVEN_CLI_OPTS test -pl domain
-Dtest=ArchitectureComplianceTest#domain_no_spring
- mvn $MAVEN_CLI_OPTS test -pl domain
-Dtest=ArchitectureComplianceTest#domain_no_infrastructure
# P1 Checks — Warn on failure
- mvn $MAVEN_CLI_OPTS test -pl domain
-Dtest=ArchitectureComplianceTest#layered_architecture ||
echo "⚠️ P1 violation detected, manual review required"
# P2 Checks — Report only
- mvn $MAVEN_CLI_OPTS test -pl domain
-Dtest=ArchitectureComplianceTest#naming_conventions ||
echo "📋 P2 violation logged for review"
artifacts:
when: always
reports:
junit: domain/target/surefire-reports/TEST-*.xml
paths:
- domain/target/surefire-reports/
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
- if: $CI_COMMIT_BRANCH == "main"
when: always
- when: manual
# ──── Integration Tests ────
integration-test:
stage: test
services:
- postgres:16-alpine
- redis:7-alpine
variables:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/testdb
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: test
SPRING_CACHE_REDIS_HOST: redis
script:
- mvn $MAVEN_CLI_OPTS verify -pl infrastructure
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
# ──── OWASP Dependency Check ────
dependency-security:
stage: security
script:
- mvn $MAVEN_CLI_OPTS org.owasp:dependency-check-maven:check
rules:
- if: $CI_COMMIT_BRANCH == "main"
# ──── Deploy to Staging ────
deploy-staging:
stage: deploy
image: bitnami/kubectl:1.29
script:
- kubectl set image deployment/$CI_PROJECT_NAME
app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -n staging
- kubectl rollout status deployment/$CI_PROJECT_NAME -n staging
environment:
name: staging
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manualPipeline Flow Diagram
┌──────────────┐
│ Commit │
└──────┬───────┘
▼
┌──────────────┐
│ Compile │
└──────┬───────┘
▼
┌──────────────┐
│ Architecture│ ← ArchUnit P0 blocks here
│ Validation │
└──────┬───────┘
▼
┌─────────────┴─────────────┐
│ │
▼ ▼
┌──────────────┐ ┌────────────────┐
│ Integration │ │ Security Scan │
│ Tests │ │ (parallel) │
└──────┬───────┘ └───────┬────────┘
│ │
└─────────────┬─────────────┘
▼
┌──────────────┐
│ Deploy │ (manual gate)
│ Staging │
└──────────────┘Grafana Domain Health Dashboard
用于领域事件监控的 Grafana 仪表板配置。
Panels
| Panel | Metric | Type | Description |
|---|---|---|---|
| Event Publishing Rate | rate(domain_events_published_total[5m]) | Time series | 每秒事件发布速率 |
| Processing Duration | histogram_quantile(0.99, domain_events_processing_duration_seconds_bucket) | Heatmap | P99 处理延迟 |
| Outbox Depth | domain_outbox_depth | Stat | 当前 Outbox 积压数量 |
| Error Rate | rate(domain_events_error_total[5m]) | Time series | 错误率百分比 |
Alert Rules
- alert: DomainEventBacklog
expr: rate(domain_events_published_total[2m]) > 100
for: 2m
severity: critical
- alert: OutboxQueueGrowing
expr: domain_outbox_depth > 1000
for: 1m
severity: critical
- alert: EventProcessingErrorRate
expr: rate(domain_events_error_total[5m]) / rate(domain_events_published_total[5m]) > 0.05
for: 2m
severity: warning完整 Prometheus 配置见 ddd-observability-config.md。
# Kubernetes Deployment Example — DDD Microservices with CQRS
## Namespace Setup
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: ddd-orders
labels:
domain-bounded-context: orders
domain-type: core
---
apiVersion: v1
kind: Namespace
metadata:
name: ddd-payments
labels:
domain-bounded-context: payments
domain-type: core
```
## Command Service (Write Model)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-command
namespace: ddd-orders
spec:
replicas: 2
selector:
matchLabels:
app: order-command
template:
metadata:
labels:
app: order-command
domain-layer: application
spec:
initContainers:
- name: db-migration
image: flyway/flyway:10-alpine
command: ["flyway", "migrate"]
env:
- name: FLYWAY_URL
value: "jdbc:postgresql://order-db:5432/orders"
- name: FLYWAY_USER
value: "postgres"
- name: FLYWAY_PASSWORD
valueFrom:
secretKeyRef:
name: order-db-credentials
key: password
volumeMounts:
- name: migrations
mountPath: /flyway/sql
containers:
- name: command
image: registry.example.com/order-command:1.0.0
ports:
- containerPort: 8080
name: http
env:
- name: SPRING_PROFILES_ACTIVE
value: "k8s,command"
- name: SPRING_DATASOURCE_URL
value: "jdbc:postgresql://order-db:5432/orders"
- name: EVENT_BROKER
value: "kafka://kafka-cluster:9092"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 20
volumes:
- name: migrations
configMap:
name: flyway-migrations
---
apiVersion: v1
kind: Service
metadata:
name: order-command
namespace: ddd-orders
spec:
selector:
app: order-command
ports:
- port: 8080
targetPort: http
```
## Query Service (Read Model)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-query
namespace: ddd-orders
spec:
replicas: 3
selector:
matchLabels:
app: order-query
template:
metadata:
labels:
app: order-query
domain-layer: infrastructure
spec:
containers:
- name: query
image: registry.example.com/order-query:1.0.0
ports:
- containerPort: 8080
name: http
env:
- name: SPRING_PROFILES_ACTIVE
value: "k8s,query"
- name: READ_DB_HOST
value: "order-read-db"
- name: CACHE_TYPE
value: "redis"
- name: REDIS_HOST
value: "redis-cluster"
resources:
requests:
cpu: "300m"
memory: "1Gi"
limits:
cpu: "1"
memory: "4Gi"
readinessProbe:
httpGet:
path: /actuator/health
port: 8081
---
apiVersion: v1
kind: Service
metadata:
name: order-query
namespace: ddd-orders
spec:
selector:
app: order-query
ports:
- port: 8080
targetPort: http
```
## Horizontal Pod Autoscaler
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-command-hpa
namespace: ddd-orders
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-command
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-query-hpa
namespace: ddd-orders
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-query
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
```
## Network Policy — Bounded Context Isolation
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: bc-isolation
namespace: ddd-orders
spec:
podSelector:
matchExpressions:
- key: domain-layer
operator: In
values: [application, infrastructure]
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: api-gateway
egress:
- to:
- namespaceSelector:
matchLabels:
domain-bounded-context: payments
ports:
- port: 8080
```
## ConfigMap — Flyway Migration
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: flyway-migrations
namespace: ddd-orders
data:
V1__create_event_store.sql: |
CREATE TABLE IF NOT EXISTS domain_event_store (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_id VARCHAR(36) NOT NULL,
event_type VARCHAR(200) NOT NULL,
event_data JSONB NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL
);
V2__create_outbox.sql: |
CREATE TABLE IF NOT EXISTS outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id VARCHAR(36) NOT NULL UNIQUE,
payload JSONB NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
## Deployment — Per-Bounded Context Database
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: order-db
namespace: ddd-orders
spec:
serviceName: order-db
replicas: 1
selector:
matchLabels:
app: order-db
template:
metadata:
labels:
app: order-db
spec:
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_DB
value: orders
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: order-db-credentials
key: password
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
livenessProbe:
exec:
command: [pg_isready, -U, postgres]
initialDelaySeconds: 30
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: order-db
namespace: ddd-orders
spec:
selector:
app: order-db
ports:
- port: 5432
```
CI/CD ArchUnit Setup Reference
ArchUnit Maven Configuration
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>Gradle Configuration
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'Full ArchUnit Test Suite for CI/CD
@AnalyzeClasses(packages = "com.example")
public class ArchitectureComplianceTest {
// P0 — Domain purity: zero framework dependencies
@ArchTest
static final ArchRule domain_no_spring =
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAnyPackage("org.springframework..", "javax.persistence..",
"org.apache.ibatis..", "com.baomidou..");
@ArchTest
static final ArchRule domain_no_infrastructure =
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..");
@ArchTest
static final ArchRule domain_no_external_libs =
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAnyPackage("com.fasterxml.jackson..", "org.apache.http..",
"io.netty..");
// P1 — Layer dependency direction
@ArchTest
static final ArchRule layered_architecture =
layeredArchitecture()
.consideringAllDependencies()
.layer("Interface").definedBy("..interface..")
.layer("Application").definedBy("..application..")
.layer("Domain").definedBy("..domain..")
.layer("Infrastructure").definedBy("..infrastructure..")
.whereLayer("Interface").mayNotBeAccessedByAnyLayer()
.whereLayer("Application").mayOnlyBeAccessedByLayers("Interface")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure", "Interface")
.whereLayer("Infrastructure").mayOnlyBeAccessedByLayers("Application");
// P1 — No circular dependencies between modules
@ArchTest
static final ArchRule no_cycle_between_modules =
slices()
.matching("com.example.(*)..")
.should().beFreeOfCycles();
// P2 — Naming conventions
@ArchTest
static final ArchRule aggregate_root_naming =
classes()
.that().areAnnotatedWith(AggregateRoot.class)
.should().haveSimpleNameEndingWith("Aggregate")
.orShould().haveSimpleNameEndingWith("Root");
@ArchTest
static final ArchRule repository_naming =
classes()
.that().resideInAPackage("..domain..repository..")
.and().areInterfaces()
.should().haveSimpleNameEndingWith("Repository");
}CI/CD Integration
Step 1: Run ArchUnit tests as a dedicated pipeline stage
# Pipeline stage configuration
architecture-validation:
stage: test
script:
- mvn test -pl domain -Dtest=ArchitectureComplianceTest
artifacts:
reports:
junit: domain/target/surefire-reports/TEST-*.xml
expire_in: 30 daysStep 2: Configure severity-based failure policy
| Severity | CI/CD Action | Example |
|---|---|---|
| P0 | Block merge | Domain layer has Spring import |
| P1 | Warning + manual approval | Layer violation |
| P2 | Report only | Naming convention violation |
Step 3: Generate architecture report
# Generate HTML report
mvn test -pl domain -Dtest=ArchitectureComplianceTest \
-Darchunit.output.path=target/archunit-reportPerformance Considerations
- Run ArchUnit tests in parallel with other test suites
- Use
@AnalyzeClasses(importOptions = {DoNotIncludeTests.class})to skip test classes - Cache analysis results with
importOptions = {ImportOption.Predefined.ONLY_INCLUDE_SOURCE}
CI/CD Check Script
#!/bin/bash
# ci-arch-check.sh — Run architecture validation in CI
set -e
echo "=== Architecture Validation ==="
# Run all ArchUnit tests
mvn test -pl domain -Dtest=ArchitectureComplianceTest \
-Darchunit.freeze.store.default.class=com.tngtech.archunit.library.freeze.ViolationStoreFactory$InMemory
# Check for any violations
if [ $? -ne 0 ]; then
echo "❌ Architecture violations detected!"
echo "See target/surefire-reports for details."
exit 1
fi
echo "✅ Architecture validation passed."Database Migration Strategies for DDD
Migration Strategy Decision Matrix
| DDD Scenario | Strategy | Tool | Key Considerations |
|---|---|---|---|
| Layered/Onion (single DB) | Sequential migrations | Flyway/Liquibase | One migration script per schema change |
| CQRS L2 (read/write separation) | Dual migration tracks | Flyway + custom scripts | Handle replication lag, eventual consistency |
| Event Sourcing | Schema-less event store | Axon/EventStoreDB | Append-only, no migration for event data |
| Microservices + DDD | Per bounded context independent DB | Per-service Flyway | Schema-per-service, avoid cross-service joins |
| Outbox pattern | Outbox + event tables as migration seeds | Flyway | Outbox table schema must be included in migrations |
Outbox Pattern Migration
-- V1__create_outbox_for_domain_events.sql
CREATE TABLE outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_id VARCHAR(36) NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
event_type VARCHAR(200) NOT NULL,
event_id VARCHAR(36) NOT NULL,
payload JSONB NOT NULL,
trace_id VARCHAR(64),
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
processed_at TIMESTAMPTZ,
UNIQUE(event_type, event_id)
);
CREATE INDEX idx_outbox_status ON outbox(status) WHERE status = 'PENDING';
CREATE INDEX idx_outbox_created ON outbox(created_at);Adding Domain Events to Existing Database
-- V2__add_domain_event_tracking.sql
-- Step 1: Create domain event log table
CREATE TABLE domain_event_log (
id SERIAL PRIMARY KEY,
event_id VARCHAR(36) NOT NULL UNIQUE,
event_type VARCHAR(200) NOT NULL,
aggregate_id VARCHAR(36) NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
event_data JSONB NOT NULL,
metadata JSONB DEFAULT '{}',
occurred_at TIMESTAMPTZ NOT NULL,
published BOOLEAN DEFAULT FALSE
);
-- Step 2: Add migration tracking to existing tables
ALTER TABLE orders ADD COLUMN IF NOT EXISTS ddd_version INT DEFAULT 0;
ALTER TABLE orders ADD COLUMN IF NOT EXISTS last_domain_event_at TIMESTAMPTZ;
-- Step 3: Backfill past data (if needed)
INSERT INTO domain_event_log (
event_id, event_type, aggregate_id, aggregate_type,
event_data, occurred_at, published
)
SELECT
gen_random_uuid()::text,
'OrderMigrated',
id::text,
'Order',
jsonb_build_object(
'order_id', id,
'status', status,
'total_amount', total_amount
),
created_at,
TRUE
FROM orders
WHERE NOT EXISTS (
SELECT 1 FROM domain_event_log
WHERE aggregate_id = orders.id::text
AND event_type = 'OrderMigrated'
);CQRS Read Model Table Migration
-- V3__create_order_read_model.sql
-- Materialized view for CQRS query side
CREATE MATERIALIZED VIEW order_summary_mv AS
SELECT
o.id,
o.customer_id,
o.status,
o.total_amount,
COUNT(oi.id) AS item_count,
o.created_at,
o.updated_at
FROM orders o
LEFT JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id;
CREATE UNIQUE INDEX idx_order_summary_mv_id ON order_summary_mv(id);
-- Refresh function
CREATE OR REPLACE FUNCTION refresh_order_summary()
RETURNS TRIGGER AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY order_summary_mv;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Trigger on domain events
CREATE TRIGGER refresh_order_summary_on_event
AFTER INSERT OR UPDATE ON domain_event_log
FOR EACH STATEMENT
EXECUTE FUNCTION refresh_order_summary();Idempotent Migration Scripts
-- V4__add_event_store_if_not_exists.sql
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'event_store'
) THEN
CREATE TABLE event_store (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
stream_id VARCHAR(100) NOT NULL,
stream_version INT NOT NULL,
event_type VARCHAR(200) NOT NULL,
event_data JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(stream_id, stream_version)
);
RAISE NOTICE 'Created event_store table';
ELSE
RAISE NOTICE 'event_store table already exists, skipping';
END IF;
END $$;Backward-Compatible Schema Changes
-- V5__add_event_metadata_column.sql
-- Step 1: Add column as nullable (backward compatible)
ALTER TABLE domain_event_log
ADD COLUMN IF NOT EXISTS correlation_id VARCHAR(64);
-- Step 2: Populate existing rows
UPDATE domain_event_log
SET correlation_id = metadata->>'correlation_id'
WHERE correlation_id IS NULL
AND metadata ? 'correlation_id';
-- Step 3: Add NOT NULL constraint (after backfill)
ALTER TABLE domain_event_log
ALTER COLUMN correlation_id SET NOT NULL;
-- Step 4: Add index
CREATE INDEX IF NOT EXISTS idx_event_correlation
ON domain_event_log(correlation_id);Rollback Strategy
-- V5__rollback_plan.sql
-- Rollback: Drop event tracking columns from orders
-- ALTER TABLE orders DROP COLUMN IF EXISTS ddd_version;
-- ALTER TABLE orders DROP COLUMN IF EXISTS last_domain_event_at;
-- Rollback: Drop event store
-- DROP TABLE IF EXISTS event_store CASCADE;
-- Rollback: Drop outbox
-- DROP TABLE IF EXISTS outbox CASCADE;
-- Rollback: Drop domain event log
-- DROP TABLE IF EXISTS domain_event_log CASCADE;DDD Observability Configuration
Micrometer Custom Metrics
@Component
public class DomainMetricsCollector {
private final MeterRegistry registry;
public DomainMetricsCollector(MeterRegistry registry) {
this.registry = registry;
}
// Domain event publication metrics
public void recordEventPublished(String eventType, String aggregateType) {
Counter.builder("domain.events.published")
.tag("event_type", eventType)
.tag("aggregate_type", aggregateType)
.register(registry)
.increment();
}
public void recordEventProcessed(String eventType, Duration duration) {
Timer.builder("domain.events.processing.duration")
.tag("event_type", eventType)
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry)
.record(duration);
}
// Aggregate loading metrics
public Timer.Sample startAggregateLoad() {
return Timer.start(registry);
}
public void stopAggregateLoad(Timer.Sample sample, String aggregateType) {
sample.stop(Timer.builder("domain.aggregate.load.duration")
.tag("aggregate_type", aggregateType)
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry));
}
// Repository metrics
public <T> T measureRepositoryCall(String repoName, String operation,
Supplier<T> call) {
return Timer.builder("domain.repository.call.duration")
.tag("repository", repoName)
.tag("operation", operation)
.register(registry)
.record(call);
}
// Outbox monitoring
public void updateOutboxDepth(String destination, int depth) {
Gauge.builder("domain.outbox.depth", () -> depth)
.tag("destination", destination)
.register(registry);
}
}OpenTelemetry Tracing Integration
@Component
public class DomainTracingConfig {
@Autowired
private Tracer tracer;
// Trace domain event publication
public Span startEventSpan(String eventType, String aggregateId) {
Span span = tracer.spanBuilder("domain.event.publish")
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
span.setAttribute("event.type", eventType);
span.setAttribute("aggregate.id", aggregateId);
return span;
}
// Trace aggregate operations
public Span startAggregateSpan(String operation, String aggregateType,
String aggregateId) {
Span span = tracer.spanBuilder("domain.aggregate." + operation)
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
span.setAttribute("aggregate.type", aggregateType);
span.setAttribute("aggregate.id", aggregateId);
return span;
}
// Propagate context across event boundaries
public Context injectContext(Context context) {
return context;
}
}Logging Correlation
<!-- Logback pattern with trace context -->
<property name="DDD_LOG_PATTERN"
value="%d{ISO8601} [%X{traceId},%X{spanId}] %-5level %logger{36} - %msg%n"/>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdc>true</includeMdc>
<fieldNames>
<timestamp>@timestamp</timestamp>
<version>[ignore]</version>
<logger>logger.name</logger>
</fieldNames>
</encoder>
</appender>
<!-- Structured logging for domain events -->
<appender name="EVENT_JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/domain-events.json</file>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>Spring Boot Actuator Configuration
# application.yml — Actuator for DDD
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus,info
metrics:
tags:
application: ${spring.application.name}
bounded-context: ${ddd.bounded-context:unknown}
export:
prometheus:
enabled: true
distribution:
percentiles-histogram:
"[domain.*]": true
slo:
"[domain.repository.call.duration]": "10ms,50ms,100ms,500ms"
health:
probes:
enabled: trueCustom Health Checks for Domain
@Component
public class DomainHealthAggregator implements ReactiveHealthIndicator {
@Autowired
private List<HealthIndicator> domainHealthIndicators;
@Override
public Mono<Health> health() {
return Flux.fromIterable(domainHealthIndicators)
.flatMap(indicator -> Mono.fromCallable(indicator::health))
.reduce(Health.up(), (combined, next) -> {
// Aggregate health: all must be UP
if (!"UP".equals(next.getStatus().getCode())) {
return Health.down(combined)
.withDetails(next.getDetails())
.build();
}
return combined;
});
}
}AlertManager Configuration
# alertmanager.yml
route:
receiver: "domain-team"
routes:
- match:
severity: critical
receiver: "domain-oncall"
repeat_interval: 5m
- match:
severity: warning
receiver: "domain-team"
repeat_interval: 30m
receivers:
- name: "domain-oncall"
webhook_configs:
- url: "https://hooks.example.com/domain/critical"
send_resolved: true
- name: "domain-team"
slack_configs:
- channel: "#ddd-alerts"
api_url: "https://hooks.slack.com/services/xxx"Dockerfile Patterns for DDD Architectures
Pattern 1: Optimized Multi-Stage Build (Monolith DDD)
# Stage 1: Build
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /build
COPY pom.xml .
COPY start/ start/
COPY domain/ domain/
COPY application/ application/
COPY infrastructure/ infrastructure/
COPY adapter/ adapter/
RUN --mount=type=cache,target=/root/.m2 \
mvn clean package -DskipTests -pl start -am
# Stage 2: Runtime
FROM eclipse-temurin:17-jre-alpine AS runtime
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /build/start/target/*.jar app.jar
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Pattern 2: CQRS — Command Service (Write-Optimized)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY command-service/target/*.jar app.jar
# Write-optimized JVM tuning
ENV JAVA_OPTS="-Xms512m -Xmx2g \
-XX:+UseZGC \
-XX:MaxGCPauseMillis=10 \
-Dspring.datasource.hikari.maximum-pool-size=20"
HEALTHCHECK --interval=15s --timeout=5s \
CMD wget -qO- http://localhost:8080/actuator/health/readiness || exit 1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]Pattern 3: CQRS — Query Service (Read-Optimized)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY query-service/target/*.jar app.jar
# Read-optimized JVM tuning — larger heap for caching
ENV JAVA_OPTS="-Xms1g -Xmx4g \
-XX:+UseZGC \
-XX:MaxGCPauseMillis=50 \
-Dspring.datasource.hikari.maximum-pool-size=5 \
-Dspring.cache.type=caffeine"
HEALTHCHECK --interval=30s --timeout=5s \
CMD wget -qO- http://localhost:8080/actuator/health/liveness || exit 1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]Pattern 4: Sidecar Container (for Event Monitoring)
# sidecar/Dockerfile — Event monitor sidecar
FROM alpine:3.19
RUN apk add --no-cache curl jq
COPY monitor.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/monitor.sh
ENTRYPOINT ["monitor.sh"]Pattern 5: Distroless Base Image (Security-First)
FROM maven:3.9-eclipse-temurin-17-alpine AS build
WORKDIR /build
COPY . .
RUN mvn clean package -DskipTests
FROM gcr.io/distroless/java17-debian12
COPY --from=build /build/start/target/*.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]Docker Compose for Multi-Service DDD
version: "3.9"
services:
order-command:
build:
context: .
dockerfile: Dockerfile.command
ports: ["8081:8080"]
environment:
- DB_URL=jdbc:postgresql://write-db:5432/orders
- EVENT_BOOTSTRAP_SERVERS=kafka:9092
depends_on:
write-db: { condition: service_healthy }
kafka: { condition: service_started }
order-query:
build:
context: .
dockerfile: Dockerfile.query
ports: ["8082:8080"]
environment:
- ES_HOSTS=elasticsearch:9200
depends_on:
elasticsearch: { condition: service_healthy }
write-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: orders
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes: ["pgdata:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
elasticsearch:
image: elasticsearch:8.12.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
volumes: ["esdata:/usr/share/elasticsearch/data"]
kafka:
image: confluentinc/cp-kafka:7.6.0
environment:
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
volumes:
pgdata:
esdata:Image Optimization Rules
| Practice | Benefit |
|---|---|
| Multi-stage builds | Reduce image size by 60-80% |
| Distroless base | 0 CVE, smaller attack surface |
Specific tag (not :latest) | Reproducible builds |
| Add non-root user | Container security best practice |
| Layer ordering: infrequent → frequent | Better layer cache utilization |
Flyway Migration Patterns for Domain Events
Domain Event Store Table
-- V1__create_domain_event_store.sql
CREATE TABLE domain_event_store (
id VARCHAR(36) PRIMARY KEY,
aggregate_id VARCHAR(36) NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
event_type VARCHAR(200) NOT NULL,
event_version INT NOT NULL DEFAULT 1,
event_data JSONB NOT NULL,
metadata JSONB,
occurred_at TIMESTAMPTZ NOT NULL,
processed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_event_aggregate_id ON domain_event_store(aggregate_id);
CREATE INDEX idx_event_type ON domain_event_store(event_type);
CREATE INDEX idx_event_occurred_at ON domain_event_store(occurred_at);
CREATE INDEX idx_event_processed ON domain_event_store(processed)
WHERE processed = FALSE;Outbox Pattern Table
-- V2__create_outbox_table.sql
CREATE TABLE outbox_message (
id VARCHAR(36) PRIMARY KEY,
aggregate_id VARCHAR(36) NOT NULL,
event_type VARCHAR(200) NOT NULL,
payload JSONB NOT NULL,
trace_id VARCHAR(64),
destination VARCHAR(255),
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
retry_count INT NOT NULL DEFAULT 0,
max_retries INT NOT NULL DEFAULT 3,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_attempt_at TIMESTAMPTZ,
processed_at TIMESTAMPTZ
);
CREATE INDEX idx_outbox_status ON outbox_message(status)
WHERE status IN ('PENDING', 'RETRYING');
CREATE INDEX idx_outbox_created ON outbox_message(created_at);Event Sourcing Snapshots
-- V3__create_event_snapshot_table.sql
CREATE TABLE event_snapshot (
aggregate_id VARCHAR(36) PRIMARY KEY,
aggregate_type VARCHAR(100) NOT NULL,
version INT NOT NULL,
state JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Projection Table for CQRS Read Model
-- V4__create_order_read_model.sql
CREATE TABLE order_read_model (
id VARCHAR(36) PRIMARY KEY,
customer_id VARCHAR(36) NOT NULL,
customer_name VARCHAR(100),
status VARCHAR(20) NOT NULL,
total_amount DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
item_count INT NOT NULL DEFAULT 0,
paid_at TIMESTAMPTZ,
shipped_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
version INT NOT NULL DEFAULT 1
);
CREATE INDEX idx_read_model_customer ON order_read_model(customer_id);
CREATE INDEX idx_read_model_status ON order_read_model(status);Idempotency Table
-- V5__create_idempotency_table.sql
CREATE TABLE idempotency_key (
id VARCHAR(64) PRIMARY KEY,
consumer VARCHAR(100) NOT NULL,
event_type VARCHAR(200) NOT NULL,
event_id VARCHAR(36) NOT NULL,
response JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(consumer, event_type, event_id)
);
-- Auto-expire old entries (TTL equivalent)
CREATE INDEX idx_idempotency_created ON idempotency_key(created_at);Migration Script for Event-Enabling Existing Tables
-- V6__add_domain_events_to_existing_tables.sql
-- 1. Add event tracking columns to existing domain tables
ALTER TABLE orders ADD COLUMN IF NOT EXISTS last_event_type VARCHAR(200);
ALTER TABLE orders ADD COLUMN IF NOT EXISTS last_event_at TIMESTAMPTZ;
ALTER TABLE orders ADD COLUMN IF NOT EXISTS event_version INT DEFAULT 0;
-- 2. Create trigger function for event tracking
CREATE OR REPLACE FUNCTION track_domain_event()
RETURNS TRIGGER AS $$
BEGIN
NEW.last_event_at = NOW();
NEW.event_version = OLD.event_version + 1;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 3. Apply trigger
CREATE TRIGGER trg_order_event_tracking
BEFORE UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION track_domain_event();Liquibase Equivalents
<changeSet id="1" author="devops">
<createTable tableName="domain_event_store">
<column name="id" type="VARCHAR(36)">
<constraints primaryKey="true"/>
</column>
<column name="aggregate_id" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="event_type" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="event_data" type="JSONB"/>
<column name="occurred_at" type="TIMESTAMPTZ">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="domain_event_store"
indexName="idx_aggregate_id">
<column name="aggregate_id"/>
</createIndex>
</changeSet>GitLab CI DDD Pipeline Reference
Full Pipeline Configuration
stages:
- build
- unit-test
- architecture-check
- integration-test
- security-scan
- build-image
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .m2/repository/
- domain/target/
- application/target/
# Stage 1: Build
compile:
stage: build
script:
- mvn $MAVEN_CLI_OPTS compile -pl domain,application -am
artifacts:
paths:
- domain/target/
- application/target/
expire_in: 2 hours
# Stage 2: Unit Tests
unit-test:
stage: unit-test
script:
- mvn $MAVEN_CLI_OPTS test -pl domain -Dtest='*ValueObjectTest,*AggregateTest'
artifacts:
reports:
junit: domain/target/surefire-reports/TEST-*.xml
# Stage 3: Architecture Compliance (Quality Gate)
architecture-check:
stage: architecture-check
script:
- mvn $MAVEN_CLI_OPTS test -pl domain -Dtest=ArchitectureComplianceTest
artifacts:
paths:
- domain/target/archunit-report/
reports:
junit: domain/target/surefire-reports/TEST-*.xml
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
- if: $CI_COMMIT_BRANCH == "main"
when: always
# Stage 4: Integration Tests
integration-test:
stage: integration-test
services:
- postgres:16-alpine
variables:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/testdb
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: test
script:
- mvn $MAVEN_CLI_OPTS verify -pl infrastructure
-Dtest='*RepositoryImplTest'
needs: [compile, unit-test]
# Stage 5: Security Scan
security-scan:
stage: security-scan
script:
- mvn $MAVEN_CLI_OPTS verify -pl start -DskipTests
- mvn $MAVEN_CLI_OPTS dependency-check:check
rules:
- if: $CI_COMMIT_BRANCH == "main"
# Stage 6: Build Docker Image
docker-build:
stage: build-image
image: docker:24.0.5
services:
- docker:24.0.5-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == "main"
# Stage 7: Deploy to K8s
deploy:
stage: deploy
image: bitnami/kubectl:1.29
script:
- kubectl set image deployment/order-service
order-service=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
-n bc-orders
- kubectl rollout status deployment/order-service -n bc-orders
environment:
name: production/bc-orders
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manualMerge Request Approval Rules
# .gitlab/merge_request_templates/DDD_Quality_Gate.md
## Architecture Compliance Checklist
- [ ] All ArchUnit P0 tests pass ✅
- [ ] No circular dependencies detected ✅
- [ ] Domain layer has zero framework dependencies ✅
- [ ] Layered dependency direction is correct ✅
## Required Approvals
| Role | Required? |
|------|:---------:|
| Developer | ✅ |
| Senior Developer | ✅ (if P0 violation) |
| Architect | ✅ (if architecture change) |
Kubernetes Deployment Reference for DDD
Deployment Template (Bounded Context Service)
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
domain-bounded-context: orders
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
containers:
- name: order-service
image: registry.example.com/order-service:1.0.0
ports:
- containerPort: 8080
name: http
- containerPort: 8081
name: management
env:
- name: SPRING_PROFILES_ACTIVE
value: "k8s"
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: order-db-secret
key: jdbc-url
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: management
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: management
initialDelaySeconds: 20
periodSeconds: 5
startupProbe:
httpGet:
path: /actuator/health
port: management
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30
terminationGracePeriodSeconds: 60Service Template
apiVersion: v1
kind: Service
metadata:
name: order-service
labels:
app: order-service
domain-bounded-context: orders
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: http
name: http
- port: 8081
targetPort: management
name: management
selector:
app: order-servicePer-Bounded Context Namespace
apiVersion: v1
kind: Namespace
metadata:
name: bc-orders
labels:
domain-bounded-context: orders
domain-type: core
---
apiVersion: v1
kind: Namespace
metadata:
name: bc-payments
labels:
domain-bounded-context: payments
domain-type: core
---
apiVersion: v1
kind: Namespace
metadata:
name: bc-notifications
labels:
domain-bounded-context: notifications
domain-type: supportingInit Container for DB Migration
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
template:
spec:
initContainers:
- name: db-migration
image: registry.example.com/order-flyway:1.0.0
command: ["flyway", "migrate"]
envFrom:
- secretRef:
name: order-db-secret
containers:
- name: order-service
# ... main container configHorizontal Pod Autoscaler for CQRS
# Command service HPA — scale on CPU (write-bound)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-command-hpa
namespace: bc-orders
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-command
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
---
# Query service HPA — scale on memory (read/cache-bound)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-query-hpa
namespace: bc-orders
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-query
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80Network Policy (Inter-BC Isolation)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: bc-orders-isolation
namespace: bc-orders
spec:
podSelector:
matchLabels:
app: order-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: api-gateway
egress:
- to:
- namespaceSelector:
matchLabels:
domain-bounded-context: payments
ports:
- port: 8080Monitoring & Alerting Reference for DDD
Prometheus Metrics Configuration
# prometheus.yml — scrape config for DDD services
scrape_configs:
- job_name: 'ddd-services'
metrics_path: '/actuator/prometheus'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: '(order|payment|notification).*'
action: keep
- source_labels: [__meta_kubernetes_pod_label_domain_bounded_context]
target_label: bounded_context
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: serviceKey DDD Metrics
# Domain Event Metrics
domain_events_published_total:
type: counter
labels: [event_type, aggregate_type]
description: Total domain events published
domain_events_processing_duration_seconds:
type: histogram
labels: [event_type]
buckets: [0.001, 0.01, 0.05, 0.1, 0.5, 1, 5]
description: Domain event processing latency
aggregate_loading_duration_seconds:
type: histogram
labels: [aggregate_type]
buckets: [0.01, 0.05, 0.1, 0.5, 1]
description: Aggregate root loading time
repository_query_duration_seconds:
type: histogram
labels: [repository, operation]
description: Repository query latency
outbox_queue_depth:
type: gauge
labels: [destination]
description: Number of pending outbox messagesPrometheusAlerting Rules
# alerts/ddd-alerts.yml
groups:
- name: ddd-domain-alerts
interval: 30s
rules:
- alert: DomainEventBacklog
expr: |
sum by (event_type) (
rate(domain_events_published_total[5m])
) -
sum by (event_type) (
rate(domain_events_processed_total[5m])
) > 100
for: 2m
labels:
severity: warning
annotations:
summary: "Domain event backlog for {{ $labels.event_type }}"
- alert: AggregateLoadingSlow
expr: |
histogram_quantile(0.95,
rate(aggregate_loading_duration_seconds_bucket[5m])
) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "P95 aggregate loading > 500ms for {{ $labels.aggregate_type }}"
- alert: OutboxQueueGrowing
expr: outbox_queue_depth > 1000
for: 1m
labels:
severity: critical
annotations:
summary: "Outbox queue depth > 1000 for {{ $labels.destination }}"
- alert: HighEventProcessingErrorRate
expr: |
rate(domain_events_failed_total[5m]) /
rate(domain_events_processed_total[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "Event processing error rate > 5% for {{ $labels.event_type }}"
- alert: NPlusOneDetection
expr: |
rate(repository_query_count_total[1m]) /
rate(repository_aggregate_load_count_total[1m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Possible N+1 queries for {{ $labels.repository }}"Grafana Dashboard (JSON Model)
{
"title": "DDD Domain Health",
"panels": [
{
"title": "Domain Event Publication Rate",
"type": "graph",
"targets": [
{
"expr": "rate(domain_events_published_total[5m])",
"legendFormat": "{{event_type}}"
}
]
},
{
"title": "Event Processing Latency (P95)",
"type": "heatmap",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(domain_events_processing_duration_seconds_bucket[5m]))",
"legendFormat": "{{event_type}}"
}
]
},
{
"title": "Aggregate Load Times",
"type": "stat",
"targets": [
{
"expr": "histogram_quantile(0.99, rate(aggregate_loading_duration_seconds_bucket[5m]))"
}
]
},
{
"title": "Outbox Queue Depth",
"type": "gauge",
"targets": [
{
"expr": "outbox_queue_depth"
}
]
}
]
}Log Aggregation Patterns
<!-- logback-spring.xml — DDD-specific appenders -->
<configuration>
<!-- Domain event audit log -->
<appender name="DOMAIN_EVENT" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/domain-events.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/domain-events-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} | %X{traceId} | %msg%n</pattern>
</encoder>
</appender>
<!-- Aggregate performance log -->
<appender name="AGGREGATE_PERF" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/aggregate-perf.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/aggregate-perf-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} | %-5level | %msg%n</pattern>
</encoder>
</appender>
<logger name="com.example.domain.event" level="INFO" additivity="false">
<appender-ref ref="DOMAIN_EVENT"/>
</logger>
<logger name="com.example.domain.aggregate.performance" level="WARN" additivity="false">
<appender-ref ref="AGGREGATE_PERF"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>Custom DDD Health Indicator
@Component
public class DddHealthIndicator implements HealthIndicator {
private final EventBus eventBus;
private final DataSource dataSource;
private final MeterRegistry meterRegistry;
@Override
public Health health() {
Health.Builder builder = new Health.Builder();
// Domain event bus health
try {
boolean busHealthy = eventBus.ping();
builder.withDetail("eventBus", busHealthy ? "UP" : "DOWN");
if (!busHealthy) builder.down();
} catch (Exception e) {
builder.down().withDetail("eventBus", e.getMessage());
}
// Aggregate load health
double p99LoadTime = meterRegistry
.get("aggregate.load.time")
.histogram()
.takeSnapshot()
.getPercentileValues()
.stream()
.filter(p -> p.percentile() == 0.99)
.findFirst()
.map(p -> p.value())
.orElse(0.0);
builder.withDetail("aggregateP99LoadMs", String.format("%.0f", p99LoadTime));
if (p99LoadTime > 1000) {
builder.down();
}
return builder.build();
}
}Multi-Module Build Configuration for DDD
Maven Multi-Module Setup
Parent POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>ddd-project</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>domain</module>
<module>application</module>
<module>infrastructure</module>
<module>adapter</module>
<module>start</module>
</modules>
<properties>
<java.version>17</java.version>
<archunit.version>1.3.0</archunit.version>
<maven-surefire.version>3.2.5</maven-surefire.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>Domain Module POM (Zero Framework Dependencies)
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>ddd-project</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>domain</artifactId>
<dependencies>
<!-- ⚠️ No Spring, No JPA, No MyBatis -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
<forkCount>2</forkCount>
<reuseForks>true</reuseForks>
</configuration>
</plugin>
</plugins>
</build>
</project>Gradle Multi-Module Setup
// settings.gradle.kts
rootProject.name = "ddd-project"
include("domain", "application", "infrastructure", "adapter", "start")
// domain/build.gradle.kts — Zero framework dependencies
plugins {
id("java-library")
}
dependencies {
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
}
// application/build.gradle.kts
dependencies {
implementation(project(":domain"))
implementation("org.springframework:spring-tx")
}
// infrastructure/build.gradle.kts
dependencies {
implementation(project(":domain"))
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}Incremental Build Optimization
# Maven — Build only changed modules
mvn compile -pl domain,application -am
# Gradle — Parallel build
./gradlew build --parallel --max-workers=4
# Maven — Skip domain tests in fast CI
mvn verify -pl '!domain' \
-Dtest='!ArchitectureComplianceTest'
# Maven — Parallel module builds
mvn -T 4 clean install \
-DskipTests \
-pl '!adapter,!start'CI/CD Build Cache
# .github/actions/maven-cache/action.yml
name: "Maven Cache for DDD"
runs:
using: "composite"
steps:
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: |
~/.m2/repository
!~/.m2/repository/com/example
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache module build outputs
uses: actions/cache@v4
with:
path: |
domain/target/classes
application/target/classes
infrastructure/target/classes
key: ${{ runner.os }}-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-Dependency Graph Validation
# Maven — Display dependency tree
mvn dependency:tree -Dverbose | grep "com.example"
# Gradle — Display project dependencies
./gradlew :domain:dependencies --configuration runtimeClasspath
# Check for unintended domain dependencies
mvn dependency:tree -pl domain \
| grep -E "(spring|hibernate|jackson|mybatis)" \
&& echo "⚠️ Domain has external dependencies!" \
|| echo "✅ Domain is clean"Module Dependency Rules
domain: # Zero external dependencies (JDK only)
dependencies: []
rules:
- no_spring
- no_jpa
- no_mybatis
- no_infrastructure
application: # Depends on domain only
dependencies: [domain]
rules:
- no_direct_infrastructure_import
- no_business_logic_in_app_services
infrastructure: # Implements domain interfaces
dependencies: [domain]
rules:
- must_implement_all_domain_repositories
adapter: # Depends on application + domain
dependencies: [domain, application]
rules:
- no_business_logic
- protocol_conversion_only
start: # Assembles all modules
dependencies: [domain, application, infrastructure, adapter]
rules:
- no_business_logic
- minimal_code