Skip to content

build: bump org.springframework.boot from 3.5.14 to 4.0.6 in /services/payment-service#23

Closed
dependabot[bot] wants to merge 110 commits into
mainfrom
dependabot/gradle/services/payment-service/org.springframework.boot-4.0.6
Closed

build: bump org.springframework.boot from 3.5.14 to 4.0.6 in /services/payment-service#23
dependabot[bot] wants to merge 110 commits into
mainfrom
dependabot/gradle/services/payment-service/org.springframework.boot-4.0.6

Conversation

@dependabot
Copy link
Copy Markdown

@dependabot dependabot Bot commented on behalf of github May 15, 2026

Bumps org.springframework.boot from 3.5.14 to 4.0.6.

Release notes

Sourced from org.springframework.boot's releases.

v4.0.6

🐞 Bug Fixes

  • Default security is misconfigured when spring-boot-actuator-autoconfigure is present and spring-boot-health is not #50188
  • Elasticsearch Rest5Client auto-configuration misconfigures underlying HTTP client #50187
  • ApplicationPidFileWriter does not handle symlinks correctly #50185
  • RandomValuePropertySource is not suitable for secrets #50183
  • Cassandra auto-configuration misconfigures CqlSessionBuilder #50180
  • ApplicationTemp does not handle symlinks correctly #50178
  • Remote DevTools performs comparison incorrectly #50176
  • spring.rabbitmq.ssl.verify-hostname is applied inconsistently #50174
  • Whole number values are ignored when configuring min and max expected values and SLO boundaries for a distribution summary meter #50077
  • Classic starters are missing several modules #50071
  • Module spring-boot-resttestclient is missing from spring-boot-starter-test-classic #50069
  • Annotations like @Ssl don't work on @Bean methods when using @ServiceConnection #50064
  • EnversRevisionRepositoriesRegistrar should reuse @EnableEnversRepositories rather than configuring the JPA counterpart #50039
  • WebFlux Cloud Foundry links endpoint includes query string from received request in resolved links #50017
  • Imports on a containing test class are ignored when a nested class has imports #50012
  • With spring.jackson.use-jackson2-defaults set to true, FAIL_ON_UNKNOWN_PROPERTIES is enabled #49951
  • 500 response from env endpoint when supplied pattern is invalid #49946
  • Reactive MongoDB starter has a transitive dependency on the synchronous MongoDB driver #49945
  • HTTP method is lost when configuring excludes in EndpointRequest #49943
  • Honor HttpMethod for reactive additional endpoint paths #49880
  • Docker Compose support doesn't work with apache/artemis image #49869
  • Docker Compose support doesn't work with apache/activemq image #49866
  • Spring Security's PathPatternRequestMatcher.Builder is not auto-configured when using WebMvcTest and spring-boot-security-test #49854
  • API versioning path strategy should be applied path last as it is not meant to yield #49800

📔 Documentation

  • Update docs to encourage Java fundamentals for beginners that prefer to learn that way #50146
  • HTTP Service Interface Clients still document that API versioning can be configured via properties #50126
  • Link to the observability section of the Lettuce documentation is broken #50097
  • Javadoc for StaticResourceLocation.FAVICON doesn't describe icons location #50085
  • MySamlRelyingPartyConfiguration is missing a Kotlin sample #50024
  • Incorrect default value for management.httpexchanges.recording.include in configuration metadata #50019
  • Link to the Kubernetes documentation when discussing startup probes #50015
  • Typo in JdbcSessionAutoConfiguration Javadoc #49873
  • Clarify that configuration property default values are not available through the Environment #49851
  • Document the need for Liquibase and Flyway starters #49839
  • Kafka documentation refers to deprecated JSON serializer and deserializer classes #49826

🔨 Dependency Upgrades

... (truncated)

Commits
  • 8821ad2 Release v4.0.6
  • 9e4048a Merge branch '3.5.x' into 4.0.x
  • 20bb11c Next development version (v3.5.15-SNAPSHOT)
  • 98daa8e Merge branch '3.5.x' into 4.0.x
  • 874f629 Fix default security with actuator but without health
  • e41b3bf Enable hostname verification for SSL connections to Elasticsearch
  • ef8527b Merge branch '3.5.x' into 4.0.x
  • 4a7bd33 Merge branch '3.5.x' into 4.0.x
  • 3a9d836 Merge branch '3.5.x' into 4.0.x
  • 8e013b6 Merge branch '3.5.x' into 4.0.x
  • Additional commits viewable in compare view

ssa1004 and others added 30 commits May 6, 2026 22:57
- Top-level: README, ARCHITECTURE, ROADMAP
- docs: ADR-style decision log, SLO, runbook skeleton
- services: order/payment/inventory placeholders with Phase 1 TODO
- modules: 4 Spring Boot Ops Toolkit module READMEs (slow-query-detector,
  correlation-mdc-starter, actuator-extras, chaos-injector) with design
  intent
- infra: docker-compose for full observability stack (Postgres, Redis,
  Kafka, OTel Collector, Prometheus, Loki, Tempo, Grafana, Alertmanager)
  plus working configs and Grafana datasource provisioning
- load: k6 baseline stub
- chaos: scenario template
- case-studies: index + per-case template
- CI: docker compose config validation + Gradle build (gated on settings.gradle)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Spring Initializr baseline: Boot 3.5.14, Gradle Kotlin DSL,
  Java 21 toolchain via foojay-resolver-convention
- Dependencies: web, data-jpa, validation, actuator, flyway-postgresql,
  Micrometer Prometheus registry, Testcontainers (postgresql)
- Flyway V1: orders + order_items with FK and indexes
- Domain: Order/OrderItem/OrderStatus with `Order.create` factory
  computing totalAmount, lifecycle hooks for created_at/updated_at
- Repository: findWithItemsById via @EntityGraph (avoid N+1 on read)
- Web: POST /orders + GET /orders/{id}, validation, GlobalExceptionHandler
- Application config: env-overridable DB host/port/user/pwd, graceful
  shutdown, prometheus + health probes endpoints
- Test: SpringBootTest + Testcontainers Postgres @Serviceconnection,
  asserts CREATE→READ round trip and Prometheus endpoint exposes
  jvm_memory_used_bytes + http_server_requests_seconds

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Activate Prometheus scrape job for order-service on
  host.docker.internal:8081 with application=order-service label
- Add Grafana dashboard "JVM + HTTP" (uid: jvm-http) auto-provisioned
  via existing dashboards path: heap by area, GC pause, threads,
  process/system CPU, request rate, p95 latency, 5xx error rate.
  Variable $service uses application label (multi + All).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ROADMAP: split Phase 1 into 4 vertical-slice steps; mark Step 1 done
- services/order-service/README: actual run/test instructions, status
  per endpoint, Step 1 checklist completed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Boot 3.5 removed `org.springframework.boot.test.autoconfigure.actuate.metrics.AutoConfigureMetrics`. The annotation is no longer needed because metrics export is controlled via `management.prometheus.metrics.export.enabled` in application.yml. Also drop the empty @DynamicPropertySource placeholder left over from the initial scaffold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Spring Initializr baseline matching order-service: Boot 3.5.14,
  Gradle Kotlin DSL, Java 21 toolchain, Micrometer Prometheus,
  Testcontainers (postgresql)
- Flyway V1: payments table (order_id, status, external_ref,
  failure_reason, attempts, completed_at)
- Domain: Payment.pending(...) factory, lifecycle markSuccess/markFailed
- Web: POST /payments returns 201 on SUCCESS, 402 on FAILED
  (HTTP semantics: PG declined the charge but request was processed)
- PgClient: RestClient with configurable connect/read timeouts,
  wraps ResourceAccessException into PgFailureException; PaymentService
  catches and marks payment FAILED (no transaction rollback)
- MockPgController: simulates external PG with configurable
  latency (gaussian: mean+stddev), failure-rate (5xx),
  timeout-rate (sleep > read timeout). Disabled via
  mini-shop.mock-pg.enabled=false in production-like envs.
  GET /mock-pg/config exposes current params for case-study reproducibility.
- Tests:
  - PaymentServiceApplicationTests: full app boot with
    @MockitoBean PgClient covering happy path + PG failure path
    + Prometheus endpoint
  - MockPgControllerTests: deterministic params (failure-rate=0)
    asserting /mock-pg/charge returns ok

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- prometheus.yml: activate payment-service scrape on
  host.docker.internal:8082 with application=payment-service label
- ROADMAP: mark Step 2 done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Spring Initializr baseline: Boot 3.5.14, Gradle Kotlin DSL,
  Java 21 toolchain, redisson-spring-boot-starter 3.31.0,
  Micrometer Prometheus, Testcontainers (postgresql + generic redis)
- Flyway V1: inventories (with @Version) + inventory_reservations
  with UNIQUE (order_id, product_id). V2 seeds productId 1001/1002/1003.
- Domain: Inventory.reserve/release ops, InventoryReservation factory
  + idempotent release (no-op when already RELEASED)
- DistributedLockService: Redisson wrapper, tryLock(wait, lease)
  configurable via INVENTORY_LOCK_*; emits
  inventory_lock_acquire_seconds{outcome=acquired|timeout|interrupted}
  Micrometer timer
- InventoryService: lock-then-transaction order via TransactionTemplate
  (avoids holding DB conn during lock wait). Idempotency on
  (orderId, productId) makes reserve/release safe under retries,
  Kafka redelivery, and SAGA compensation.
- HTTP semantics:
    201 new reservation, 200 idempotent reserve,
    409 OUT_OF_STOCK (with available qty in body),
    503 LOCK_TIMEOUT, 404 PRODUCT_NOT_FOUND / RESERVATION_NOT_FOUND
- Tests with Postgres + Redis containers covering:
    full reserve/release lifecycle with double-call idempotency,
    out-of-stock 409, prometheus exposes JVM/HTTP/lock metrics

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- prometheus.yml: activate inventory-service scrape on
  host.docker.internal:8083 with application=inventory-service label
- ROADMAP: mark Step 3 done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ClientsConfig: paymentRestClient + inventoryRestClient with
  per-client connect/read timeouts. TransactionTemplate bean for
  short-lived TX outside HTTP calls (avoid holding DB conns during
  external requests).
- PaymentClient: POST /payments returning either 201 SUCCESS or
  402 FAILED is treated as a parseable PaymentResult; other status
  codes and ResourceAccessException become PaymentInfraException.
- InventoryClient: 409 from inventory-service surfaces as
  OutOfStockException (distinct from infra failures); release() is
  best-effort — logs an orphan-reservation warning rather than
  bubbling, since callers are already in failure cleanup.
- OrderService: orchestrates reserve→pay→commit; on any failure
  releases successfully-reserved items only. Order is persisted
  PENDING then transitioned to PAID/FAILED in separate short
  transactions outside the HTTP call window.
- OrchestrationException carries the persisted Order so the handler
  returns the FAILED order body alongside the appropriate status.
- HTTP status mapping (via GlobalExceptionHandler):
    201 PAID / 402 PAYMENT_DECLINED / 409 OUT_OF_STOCK
    / 502 PAYMENT_INFRA / 503 INVENTORY_INFRA
    + X-Order-Outcome header for client-side classification.
- order_orchestration_seconds{outcome} Micrometer timer.
- Tests: happy path (201/PAID), out-of-stock with partial-reserve
  compensation (release only the items that succeeded; payment never
  called), payment-declined with full compensation, prometheus
  endpoint exposes the orchestration timer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README: Quick Start now covers the 3-service flow with curl
  examples for happy path, payment-declined, and out-of-stock,
  plus where to look in Grafana
- load/baseline.js: realistic POST /orders with random items from
  the V2 seed; thresholds count only 5xx as failure (4xx/2xx
  outcomes are intended business results)
- ROADMAP: mark Phase 1 done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ep 1)

All three services get OpenTelemetry Spring Boot starter (2.10.0) plus
the Logback OTLP appender, exporting traces and logs to the existing
otel-collector → Tempo / Loki pipelines.

Per service:
- build.gradle.kts: OTel instrumentation BOM (alpha, includes
  starter + logback-appender), starter + logback-appender
  dependencies, OTEL_SDK_DISABLED=true in test JVM env so unit/IT
  tests don't try to export
- application.yml:
    otel.service.name = ${spring.application.name}
    otel.exporter.otlp.endpoint = http://${OTEL_HOST:localhost}:4318
    otel.metrics.exporter = none  (Micrometer/Prometheus stays the
                                   metrics path; avoid double-exporting)
    otel.logs.exporter = otlp
    otel.traces.exporter = otlp
    otel.instrumentation.micrometer.enabled = false
    otel.instrumentation.logback-appender.experimental-log-attributes = true
- logback-spring.xml (new):
    CONSOLE pattern includes [%X{trace_id}/%X{span_id}] inline so
    grep on local stdout still works
    OTEL appender forwards LogRecords through the SDK so they flow to
    Loki with the active span context attached

Inherits: existing otel-collector pipelines (traces→Tempo,
logs→Loki, metrics→Prometheus OTLP receiver) + Grafana datasource
provisioning that wires tracesToLogs / derivedFields between Tempo
and Loki by trace ID.

ADRs:
- ADR-007: chose starter over Java agent (lower demo friction; revisit
  in Phase 4 K8s)
- ADR-008: OTel metrics off because Prometheus path already exists
  (avoids same metric under two names)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README: new "메트릭 / 로그 / 트레이스 한 화면에서 보기" section
  walks through the Grafana → Tempo → Loki jump path; status table
  records OpenTelemetry coverage
- ROADMAP: Phase 2 split into 4 sub-steps, Step 1 marked done
- decision-log: ADR-007 (starter vs agent) and ADR-008 (Prometheus
  is the metrics path; OTel metrics export disabled)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three rule groups in infra/prometheus/alerts.yml:

  latency-and-errors
    - order_p99_latency_high  (p99 > 500ms for 5m, P2)
    - order_error_rate_spike  (5xx ratio > 1% for 5m, P1)
      4xx is intended business outcome (409/402); only 5xx counts.

  runtime-saturation
    - hikari_pool_saturation  (active/max > 0.9 for 3m, P2)
    - jvm_gc_pause_too_long   (avg pause > 200ms for 5m, P2)

  business
    - inventory_lock_timeout_high  (timeout ratio > 1% for 3m, P2)

Each rule carries severity, service labels, summary/description and a
runbook_url pointing at docs/runbook/<alert-name>.md so an Alertmanager
notification can deep-link straight to the response procedure.

Alertmanager (infra/alertmanager/config.yml):
  - severity-based routing: P1 -> critical, P2 -> default
  - critical receiver has 0s group_wait + 30m repeat_interval
  - default receiver groups for 1m + 1h repeat
  - both receivers point at webhook placeholders (localhost:9999)
    so users plug in Slack/PagerDuty/etc. without touching routing
  - inhibit_rules suppress P2 of the same alertname while a P1 is
    firing (avoids noisy double-paging)

YAML lint passes; CI lint-config job runs promtool/amtool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (Phase 2 Step 2)

One markdown per alert. Each runbook walks an on-call through:
- the firing PromQL,
- the first-5-minute Grafana/Tempo/Loki path to look at,
- a hypothesis tree (lock contention vs slow query vs GC pressure
  vs upstream timeout, etc.),
- short-term mitigations vs the permanent fix,
- when to graduate the incident into case-studies/.

Files:
  docs/runbook/order-p99-latency-high.md
  docs/runbook/order-error-rate-spike.md
  docs/runbook/hikari-pool-saturation.md
  docs/runbook/gc-pause-too-long.md
  docs/runbook/inventory-lock-timeout-high.md
  docs/runbook/README.md  (now indexes the 5 + lists future candidates)

ROADMAP: Phase 2 Step 2 marked done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
order-service publishes OrderCreated / OrderPaid / OrderFailed via the
transactional outbox pattern: aggregate change and event row are
written in the same DB transaction; a separate scheduled poller
drains pending rows to Kafka.

- spring-kafka starter + testcontainers-kafka (test scope)
- Flyway V2: outbox_events (status, attempts, last_error, partial
  index on PENDING)
- OutboxEvent entity, OutboxStatus enum, OutboxRepository with
  native FOR UPDATE SKIP LOCKED query (PostgreSQL) so multiple
  app instances can run pollers safely
- OutboxService.enqueue is @transactional(MANDATORY) — must be
  called inside the order TX, otherwise the whole point is lost
- OutboxPoller (@scheduled, fixed delay): batch read, send to
  Kafka, mark SENT; per-event failure increments attempts, switches
  to FAILED after max-attempts. outbox.publish counter tags by
  topic + outcome (sent/retry/failed/interrupted)
- KafkaConfig enables @scheduled and binds OutboxProperties
- OrderService now writes OrderCreated in the save TX and
  OrderPaid/OrderFailed in the markPaid/markFailed TX — keeps
  Order state and outbox row atomic. Failure-path reasons
  ("OUT_OF_STOCK: ...", etc.) propagated into the OrderFailed event.
- application.yml: spring.kafka.* (idempotent producer, acks=all,
  StringSerializer for both key and value), mini-shop.outbox.poller.*
- src/test/resources/application-test.yml + Gradle env
  SPRING_PROFILES_ACTIVE=test disable the poller and Kafka listener
  auto-startup so tests don't need a broker

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
payment-service publishes PaymentSucceeded / PaymentFailed events to
Kafka after every charge, immediately after the DB transaction commits.

- spring-kafka starter + spring-kafka-test
- PaymentEvent record (type, paymentId, orderId, userId, amount,
  status, externalRef, failureReason, attempts, occurredAt) with
  factory PaymentEvent.from(Payment) that sets type by status
- PaymentEventPublisher: KafkaTemplate-based, fire-and-forget;
  whenComplete callback emits payment.event.publish counter tagged
  by type+outcome and warn-logs failures
- PaymentService now registers a TransactionSynchronization.afterCommit
  hook — publish runs only if the DB TX actually committed, so we
  never publish a PaymentSucceeded without the row being there
  (ADR-009: best-effort here, full outbox is order-service's role)
- application.yml: spring.kafka.* (idempotent producer, acks=all)
- test-profile yml + SPRING_PROFILES_ACTIVE=test disable Kafka
  listener auto-startup; PaymentServiceApplicationTests adds a
  @MockitoBean for PaymentEventPublisher so tests don't need a
  real broker

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
inventory-service publishes InventoryReserved / InventoryReleased
events to Kafka after every reserve/release operation. Publish runs
outside the lock+TX block (best-effort), event payload includes
the idempotent flag so consumers can distinguish first-time
operations from idempotent replays.

- spring-kafka starter + spring-kafka-test
- InventoryEvent record (type, reservationId, productId, orderId,
  quantity, status, idempotent, occurredAt) + reserved()/released()
  factories
- InventoryEventPublisher: KafkaTemplate-based, fire-and-forget;
  inventory.event.publish counter tagged by type+outcome
- InventoryService.reserve/release now publish after lock+TX
  completes (lock already released, TX committed, then publish)
- application.yml: spring.kafka.* matching the other services
- test-profile yml + SPRING_PROFILES_ACTIVE=test for Kafka listener;
  InventoryServiceApplicationTests adds @MockitoBean for
  InventoryEventPublisher

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- decision-log:
    ADR-009: Outbox lives in order-service (single-source-of-truth);
             payment/inventory use afterCommit publish
             (best-effort, escalate later if SAGA changes the flow)
    ADR-010: Producer idempotence + acks=all instead of Kafka
             transactions; consumer-side idempotency comes from
             domain keys (orderId, productId)
- ARCHITECTURE: services table now mentions outbox vs afterCommit
  publishing; data-stores section enumerates topics + events and
  links the publishing strategy back to ADR-009
- ROADMAP: Phase 2 Step 3 split into 3a (this PR — outbox + lifecycle
  events, sync REST flow unchanged) and 3b (full Kafka choreography
  + payment/inventory promoted to outbox)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p 4)

Wrote up the first real cross-service trace analysis as a case study.

Scenario: bumped MOCK_PG_LATENCY_MEAN_MS to 1500 (mean 1500ms,
σ=400ms) while running k6 baseline. order_p99_latency_high (P2) and
order_error_rate_spike (P1) fired together within 5 minutes.

Tempo trace + Loki side-by-side showed the failure mode: order's
read timeout to payment is 5s, payment's read timeout to mock-pg
is also 5s. When mock-pg responds at 4.9s, order has already
given up and started compensating, but payment's TX commits
SUCCESS a beat later. Result: orders.status=FAILED with
payments.status=SUCCESS for the same orderId.

The case study walks through:
- the alarms that fired and the runbook entry point
- Loki / Tempo queries used in the first 5 minutes
- the timeline reconstruction proving the in-doubt window
- the timeout-monotonicity principle (caller >= callee)
- two mitigations: short-term (drop payment->PG read timeout to 3s)
  and long-term (Phase 2 Step 3b — make payment async, this case
  is now the direct motivation)
- four follow-up tasks logged
- the Saltzer/Reed/Clark end-to-end argument as the underlying frame

Updated INDEX.md (by-date and by-tag) to link the new entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The status table at the top of README had become a jargon dump that
told a recruiter what's there but not what they'd see if they ran
it. Rewrote to lead with the story:

- README now opens with a "90초 데모" paragraph painting the actual
  experience (one POST /orders, watch the trace span across 4 services,
  click into Loki, hit a chaos scenario, see how alarms route into
  runbooks and how the first case study came out of it). The status
  table is replaced by a compact phase summary; deep dives stay where
  they belong (per-service READMEs, ADRs, case studies).
- ARCHITECTURE: same narrative shift — components / flow / mappings
  presented in the order someone wants to read them, with explicit
  links into ADRs and the new case study. The "future async flow"
  section is now framed as motivated by the case study finding rather
  than as a generic plan.
- ROADMAP: Phase 2 banner notes only Step 3b remains; Step 4 marked
  done with a link to the case study and the timeout-monotonicity
  takeaway.

No code or behavior changes. Pure prose / structure cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tep 3b)

The 2026-05-07 case study found that order/payment can disagree
(Order=FAILED, Payment=SUCCESS) when the order->payment HTTP read
timeout fires while payment's TX still commits. The case study
explicitly listed a reconciliation job as a follow-up. This change
implements it.

order-service:

- Flyway V3: payment_inbox + inventory_inbox tables, both with
  UNIQUE on the source-side id (payment_id / reservation_id) so
  the same Kafka event can be redelivered safely.
- PaymentInboxRecord, InventoryInboxRecord entities + repositories.
  Inboxes are explicitly NOT a domain truth — they're a mirror of
  outside signals, used only for cross-service comparison.
- @KafkaListener on payment.events + inventory.events:
    * groupId per topic so offsets are independent
    * upsert via existsBy* check (no @SQL hint needed)
    * inbox.consume{topic, outcome=stored|duplicate|parse_error|...}
      counter for visibility
    * @JsonIgnoreProperties(ignoreUnknown=true) on the inbound DTOs
      so we tolerate other services adding fields
- ReconciliationJob: @scheduled, scans recent PaymentSucceeded inbox
  rows and flags any whose Order is FAILED (the exact case-study
  failure mode). Emits reconciliation.inconsistency{kind=...} —
  the Phase 2 alarm set can later add a rule on this counter.
  Auto-correction is intentionally NOT done (ADR-011: human signal,
  not automated reconciliation).
- application.yml: Kafka consumer (StringDeserializer, manual ack,
  earliest reset) + reconciliation properties
- @EnableKafka on KafkaConfig
- test profile yml disables both poller and reconciliation so test
  context boots without a broker

Docs:
- ADR-011 — inbox + reconciliation design + why no auto-correction
- ROADMAP — Step 3b done as inbox/reconciliation; full async flow
  renamed to Step 3c
- case study — reconciliation follow-up checked off, links to the
  new ReconciliationJob

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- decision-log: ADR-011 — inbox + reconciliation, why we monitor
  rather than auto-correct
- ROADMAP: Step 3b is now the inbox/reconciliation work; full async
  choreography moved to Step 3c
- case study: reconciliation follow-up checked off with a link to
  the new ReconciliationJob

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A standalone Spring Boot starter (io.minishop:slow-query-detector:
0.1.0-SNAPSHOT) that, once on the classpath, wraps every DataSource
bean and emits Micrometer signals plus WARN logs for slow queries
and suspected N+1 patterns.

Module layout:
- build.gradle.kts — Java 21 toolchain, Boot 3.5.14 BOM,
  java-library + maven-publish; api dep on net.ttddyy:datasource-proxy:1.10.1,
  compileOnly for spring-boot-autoconfigure / spring-jdbc / micrometer
  (host app brings them)
- AutoConfiguration:
    @autoConfiguration(after = DataSourceAutoConfiguration.class)
    + ConditionalOn(class/bean) so it activates only with a real
    DataSource and a MeterRegistry; togglable via
    `mini-shop.slow-query.enabled=false`
- DataSourceProxyPostProcessor:
    BeanPostProcessor wrapping every DataSource with ProxyDataSourceBuilder
- SlowQueryListener:
    afterQuery hook records query_execution_seconds{outcome=ok|slow}
    timer; over slow-threshold -> slow_query_total counter + WARN
    (with caller stacktrace if enabled, framework frames filtered);
    same normalized SQL hitting n-plus-one-threshold counts ONCE on
    the threshold crossing (not per repetition) so the metric is
    "unique N+1 cases per minute" rather than "executions"
- SqlNormalizer:
    regex-based: numeric / string literals -> ?, whitespace collapsed,
    lowercased; not a real SQL parser, but enough to bucket N+1
    candidates (decision documented in DESIGN.md)
- NPlusOneContext:
    per-thread Map<normalized SQL, count>;
    TransactionSynchronizationManager.afterCompletion clears it on
    @transactional exit; non-TX callers can leak ThreadLocal — known
    limit, accepted because real N+1 lives inside a TX (lazy loading)
- ConfigurationProperties:
    mini-shop.slow-query.enabled / .slow-threshold (Duration) /
    .n-plus-one-threshold / .capture-stacktrace / .stacktrace-depth
- META-INF/spring/...AutoConfiguration.imports for Boot 3 SPI
- 13 tests (SqlNormalizer / SlowQueryListener / SpringBoot wiring).
  All green. Listener tests use ExecutionInfo.setElapsedTime
  directly; auto-config tests use ApplicationContextRunner with H2.

Docs:
- README — usage, config knobs, exposed metrics, known limits
- DESIGN.md — 8 numbered sections covering the why behind each
  decision (DataSource layer vs Hibernate stats vs Repo AOP;
  datasource-proxy vs p6spy; threshold-edge counting; regex
  normalizer vs JSqlParser; ThreadLocal cleanup; v0.2 candidates)
- decision-log: ADR-012
- ROADMAP — Phase 3 split into 5 steps; Step 1 done; subsequent
  steps cover applying the module to services, then the other three
  modules

Verified `./gradlew test` and `./gradlew publishToMavenLocal`
produce a usable starter at io.minishop:slow-query-detector:
0.1.0-SNAPSHOT (jar + sources + javadoc + pom + module metadata).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The slow-query-detector starter is now wired into order-service via
a Gradle composite build (settings.gradle.kts: includeBuild "../../
modules/slow-query-detector"). No mavenLocal needed — local and CI
build the module fresh as a sub-project. The service depends on it
as `implementation("io.minishop:slow-query-detector")` (no version,
composite resolves it).

Intentional N+1 demo:
- OrderRepository.findAllByOrderByCreatedAtDesc(Pageable) — naive
  paged listing with no @EntityGraph. Comment explicitly calls out
  this is the demo path; production use must use fetch joins.
- OrderService.listRecent(int size)
- OrderController GET /orders?size=N — response serialization
  triggers items lazy load per order (classic N+1).

Test coverage:
- New IT (naiveListEndpoint_triggersNPlusOneDetector): creates 5
  orders, calls GET /orders, asserts the n_plus_one_total counter
  has incremented. Uses the auto-injected MeterRegistry.
- All previous order-service tests still pass (sync orchestration
  paths unchanged).

Also picks up ADR-012 (slow-query-detector at the DataSource
layer — JPA/JDBC/MyBatis path-agnostic) which was authored in
Step 1 but missed in that commit's `git add`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ep 2)

- infra/grafana/dashboards/slow-query.json: dedicated dashboard
  auto-provisioned by the existing dashboards path.
  Panels: N+1 detected (5m) stat, slow queries (5m) stat, query
  rate by outcome, query latency p95, N+1 events over time.
  Variable $service uses application label.
- infra/prometheus/alerts.yml: new rule n_plus_one_detected (P2)
  in mini-shop.business group. Fires when increase(n_plus_one_total[5m])
  > 0 sustained 5m. Description points operators at the WARN log
  pattern that includes a filtered caller stacktrace, so the
  offending repository method shows up in seconds.
- docs/runbook/n-plus-one-detected.md: when fires / impact /
  Loki and Tempo first-5-min queries / hypothesis tree
  (JPA lazy load vs findById in loop vs OSIV behavior) /
  fix options (@EntityGraph, fetch join, @batchsize, projections).
- docs/runbook/README.md indexes the new runbook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
전문용어를 살리면서 처음 보는 사람도 이해할 수 있도록 짧은 풀어쓰기를 옆에 붙였다.
"기술 용어 + (간단한 설명)" 형태로 한 글의 첫 등장 시 한 번씩.

주요 변경:
- README / ARCHITECTURE / ROADMAP: trace/span/SLO/Outbox/Saga/p99/idempotent 등 핵심 용어 글로싱
- docs/decision-log.md (ADR 12개): 결정 배경의 in-doubt window, Aggregate, vendor lock-in 등
- docs/runbook/* (6개): GC pause, Hikari 포화, 분산락 timeout, N+1 등 진단 절차의 용어
- case-studies/2026-05-07-payment-timeout-race.md: in-doubt 윈도우, timeout 단조 감소 원칙
- modules/slow-query-detector (README + DESIGN + Java 코드 주석): N+1, ThreadLocal, BeanPostProcessor
- services/*: OrderService / InventoryService / PaymentService / OutboxPoller / ReconciliationJob 등 핵심 클래스 Javadoc
- SQL 마이그레이션 (V1 inventories, V2 outbox, V3 inboxes): UNIQUE 제약·partial index 의도 명시
ssa1004 and others added 10 commits May 16, 2026 13:31
order-service 의 Java → Kotlin 점진 마이그레이션 준비.
- kotlin("jvm") + plugin.spring + plugin.jpa + jvmToolchain(21)
- jackson-module-kotlin (data class → JSON 호환)
- src/main/kotlin / src/main/java 양쪽 컴파일 활성

마이그레이션 자체는 후속 commit 들 (group 별 sub-package).
… files)

Java → Kotlin 100% 마이그레이션 1차 그룹 (도메인 격리 계층).

- domain (3): Inventory / InventoryReservation (mutable aggregate + @get:JvmName +
  companion @JvmStatic 팩토리), ReservationStatus (enum class)
- exception (4): ProductNotFound / ReservationNotFound (단순 메시지 예외),
  OutOfStockException (@get:JvmName 으로 productId/requested/available 접근자
  Java 호환 유지), GlobalExceptionHandler

build.gradle.kts 에 kotlin("jvm") + plugin.spring + plugin.jpa 추가, jvmToolchain(21),
kotlin-reflect / jackson-module-kotlin runtime 의존성.

비즈니스 로직 변경 없음 — reserve / release / canReserve 동작 보존, 패키지명
io.minishop.inventory.* 유지, 호출자 (Java 측) 시그니처 100% 호환.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(4 files)

도메인 의존 계층 변환.

- repository (2): JpaRepository<T, ID> 를 Kotlin interface 로. `Optional<T>` 반환은
  그대로 유지 (호출자가 .orElseThrow 사용 중).
- service (2):
  - DistributedLockService: Redisson `RLock.tryLock(...)` Java API 그대로 호출,
    nested `LockAcquisitionException` 클래스 유지 (GlobalExceptionHandler 에서 참조).
  - InventoryService: `ReservationOutcome` 을 nested `@JvmRecord data class` 로 변환 —
    Java 호출자의 `outcome.reservation()` / `outcome.idempotent()` 시그니처 100% 유지.
    `TransactionTemplate.execute { }` 의 nullable 결과는 `!!` 로 unwrap (트랜잭션이
    예외 없이 끝나면 항상 non-null).

비즈니스 로직 변경 없음 — 락 → 트랜잭션 진입 순서, 멱등 시 RESERVED/RELEASED reservation
그대로 반환, OutOfStockException 던지는 조건 모두 동일.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…il (5 files)

build.gradle.kts 에 kotlin("jvm")/plugin.spring/plugin.jpa 2.1.0 추가, jvmToolchain(21).
domain (Payment / PaymentStatus), exception (GlobalExceptionHandler / PaymentNotFoundException),
util (LogIds) 변환. inventory / order Group 1 과 같은 패턴 — JPA aggregate 는 일반 class +
private constructor + private set + @get:JvmName, enum 은 enum class, 예외는 class :
RuntimeException, util 은 object + @JvmStatic.

호출자 (Java) 호환: Payment 의 record-style getter 는 @get:JvmName("getXxx") 로 기존
bean-style 보존. pending(...) factory 는 @JvmStatic. LogIds 는 Long? 단일 시그너처로
통합 (Java caller 는 boxing 으로 호출 가능 — 기존 사용처 없음).

빌드: ./gradlew check -x test BUILD SUCCESSFUL (테스트는 Docker daemon 미기동으로 제외).
Group 1 — 가장 격리적인 sub-package 부터.

도메인 (3):
- OrderStatus: enum class
- OrderItem / Order: JPA entity → class private constructor + @JvmStatic
  factory + @get:JvmName (Java caller 호환 — getId / getUserId / ...).
  protected no-arg constructor + private set field 보존 (JPA 가 reflection
  으로 인스턴스화하므로 var + private set 형태가 데이터 멱등성을 유지).

예외 (3):
- OrderNotFoundException: class : RuntimeException
- OrchestrationException: outcome + order 필드 @get:JvmName 유지
  (GlobalExceptionHandler / OrderService 등 Java 호출자가 .getOutcome /
   .getOrder 로 그대로 접근)
- GlobalExceptionHandler: @RestControllerAdvice idiomatic Kotlin
  (when expression, linkedMapOf)

호환성:
- io.minishop.order.* 패키지명 / public API 표면 (메서드 시그너처) 보존
- Java 호출자 (OrderService / OutboxService / 테스트) 무변경 컴파일
- JPA / Spring annotation 모두 동작
…nt DTO (2 files)

- InventoryEvent (Java record → @JvmRecord data class) — JSON 직렬화 형태가 외부 Kafka
  토픽 계약이므로 필드명 / 순서 / NON_NULL 정책 보존. companion `@JvmStatic reserved` /
  `released` 팩토리.
- InventoryEventPublisher (Kotlin @component) — `KafkaTemplate.send` 의
  CompletableFuture chain (`whenComplete { _, ex -> ... }`) Kotlin lambda 로 변환.
  발행 / 직렬화 실패 시 로깅 + Micrometer counter 증가 동작 동일.

Kafka 토픽 (`inventory.events`) / payload / 메트릭 키 (`inventory.event.publish`) 변경 없음.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…8 files)

마지막 그룹. inventory-service 100% Kotlin 완료 (21/21).

- config (2):
  - InventoryProperties (Java record → @JvmRecord data class) — @ConfigurationProperties
    binding 호환.
  - InventoryConfig — `transactionTemplate` 빈 정의.
- web (5):
  - 4개 DTO record → @JvmRecord data class. validation 어노테이션은 `@field:NotNull` /
    `@field:Min` / `@field:Max` 로 백킹 필드에 부착 (Kotlin record 컴포넌트가 기본적으로
    프로퍼티 site 에만 부착되는 문제 회피).
  - InventoryController — @RestController 클래스. ReserveRequest / ReleaseRequest 의
    `Long?` / `Int?` 필드는 @field:NotNull 통과 후이므로 `!!` 로 unwrap 해 서비스에 전달.
- main (1): InventoryServiceApplication — companion main → top-level `main(args)` 함수.

비즈니스 로직 / Bean 정의 / HTTP 경로 / 검증 규칙 전부 보존. order-service 가 호출하는
POST `/inventories/reserve` / `/inventories/release` 의 입력 / 출력 JSON 스키마 동일.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… files)

PaymentRepository (interface), PaymentService (@service @transactional),
PgClient (@component + nested PgFailureException) 변환. inventory / order
Group 2 와 같은 패턴 — interface 는 그대로, Spring bean 은 plugin.spring 이
final → open 자동 처리 (PaymentService 는 @transactional 메소드의 명시적
open 유지).

호환성: PgClient.PgFailureException 두 생성자 (String / String+Throwable)
시그너처 유지 (PaymentServiceApplicationTests 의 thenThrow 호출 호환).
Payment.markSuccess / markFailed parameter 를 String? 로 변경 — PG 응답이
nullable reference / reason 을 그대로 흘려보낼 수 있도록 (Java caller 행동
동일).

빌드: ./gradlew check -x test BUILD SUCCESSFUL.
outbox/OutboxEvent (JPA entity), OutboxStatus (enum), package-info → package.kt (Kotlin
package doc), kafka/dto/PaymentEvent (@JvmRecord data class — Java record-style accessor
호환), kafka/PaymentEventPublisher (@component + open for @MockitoBean).

호환성: PaymentEvent 는 Jackson 직렬화 시 record component 와 동일한 JSON 키 순서.
TOPIC / TYPE_SUCCEEDED / TYPE_FAILED 상수 보존. OutboxEvent factory / getter Java
signature 유지. PaymentEventPublisher 는 open + @MockitoBean (Mockito Spring) 호환.

빌드: ./gradlew check -x test BUILD SUCCESSFUL.
Group 2 — 격리적 infra utility 패키지.

concurrency (4):
- LimitExceededException: class : RuntimeException + @get:JvmName
- AdaptiveLimiterProperties: @ConfigurationProperties — Java record 의
  compact constructor 정규화 보존 (val 재대입 회피용 일반 class +
  init 블록 + custom equals/hashCode)
- AdaptiveLimiter: SimpleLimiter.newBuilder().build<Void>() 로 type arg
  지정 (concurrency-limits 의 generic 은 build() 에 붙음)
- AdaptiveLimiterInterceptor: ClientHttpRequestInterceptor idiomatic

retry (3):
- RetryProperties: 정규화 보존 (AdaptiveLimiterProperties 와 동일 패턴)
- RetryBackoff: kotlin.math.* 사용, DoubleSupplier default 인자
- RetryInterceptor: fun interface Sleeper + companion MDC 상수

호환성:
- AdaptiveLimiterPropertiesTests / RetryPropertiesTests / RetryBackoffTests
  / RetryInterceptorTests / AdaptiveLimiterTests / AdaptiveLimiterInterceptorTests
  Java 호출 그대로 통과
- Spring Binder 의 record-style binding 호환 (4-arg / 7-arg constructor)
@dependabot dependabot Bot force-pushed the dependabot/gradle/services/payment-service/org.springframework.boot-4.0.6 branch from e14f4e5 to bad8fb0 Compare May 16, 2026 04:35
ssa1004 and others added 14 commits May 16, 2026 13:36
Group 3 — 가장 작은 격리 패키지.

util (1):
- LogIds: object + @JvmStatic (Long? / Long 두 overload 보존)
  SHA-256 short hash, ADR-013 마스킹 정책

repository (1):
- OrderRepository: interface : JpaRepository<Order, Long>
  @EntityGraph(attributePaths = ["items"]) — Kotlin array literal

호환성:
- LogIdsTests 통과 (Java caller `LogIds.userId(42L)` 그대로 호출)
- JpaRepository derived query (findAllByOrderByCreatedAtDesc) 메서드명 보존
Group 4 — 이벤트 인프라 (멱등 inbox + at-least-once outbox + 정기 정합성 점검).

inbox (4):
- InventoryInboxRecord / PaymentInboxRecord: JPA entity → class
  protected constructor + @get:JvmName + companion @JvmStatic of(...)
  factory (Java caller `Inventory…Record.of(...)` 보존)
- InventoryInboxRepository / PaymentInboxRepository: interface :
  JpaRepository — derived query 메서드명 (existsByReservationId 등) 보존

outbox (6):
- OutboxStatus: enum class
- OutboxEvent: JPA entity — markSent/markAttemptFailed/markFailed +
  PRESCRIBE 의 attempts 증분 시멘틱 보존 (markSent 도 attempts +1)
- OutboxRepository: @query nativeQuery 보존 (FOR UPDATE SKIP LOCKED)
- OutboxProperties: nested class Poller — legacy sendTimeoutMs=0 → 5_000 보정
- OutboxService: @transactional(propagation = Propagation.MANDATORY) 그대로
- OutboxPoller: @scheduled fixedDelayString + @ConditionalOnProperty
  (name = ["enabled"] — Kotlin array literal)
  catch 순서 보존: InterruptedException → TimeoutException → ExecutionException

reconciliation (2):
- ReconciliationProperties: @JvmRecord data class
- ReconciliationJob: @scheduled + @ConditionalOnProperty, PageRequest.of(0, batchSize)

호환성:
- OutboxPollerFailureTests (reflection processOne) 통과
- Java caller (OrderService.outboxService.enqueue, ...) 무변경
Group 5 — 메시징 (Kafka consumer + DLT + rebalance listener).

dto (3):
- InboundInventoryEvent / InboundPaymentEvent / OrderEvent: @JvmRecord
  data class + @JsonIgnoreProperties(ignoreUnknown = true)
- OrderEvent companion 의 TYPE_CREATED/PAID/FAILED + TOPIC const

consumer (2):
- InventoryEventConsumer / PaymentEventConsumer: @KafkaListener
  topics = ["..."] — Kotlin array literal
  트랜잭션 ↔ ack 순서 (executeWithoutResult → acknowledge) 명시 보존

infra (2):
- OrderConsumerRebalanceListener: ConsumerAwareRebalanceListener override
  (non-null Collection<TopicPartition>) — Java 의 nullable annotation 부재로
  Kotlin 매핑이 non-null. AtomicInteger gauge 의 strong-reference 보존.
- KafkaConfig: DefaultErrorHandler(FixedBackOff(0L, 3)) + DLT recoverer +
  ContainerCustomizer 로 모든 listener container 에 rebalance listener 부착.
  setConsumerRebalanceListener(...) — property syntax 가 setter-only 라
  명시 호출 (Kotlin val 제약).

호환성:
- OrderConsumerRebalanceListenerTests 통과
- Kafka payload schema 무변경 (data class 가 record 와 동일 JSON shape)
Group 6 — Spring StateMachine 기반 SAGA 모델.

- OrderSagaStates / OrderSagaEvents: enum class (메서드 없음)
- OrderSagaConfig: EnumStateMachineConfigurerAdapter override —
  StateMachineStateConfigurer / StateMachineTransitionConfigurer 빌더 체인
  + Action { ctx -> ... } 람다 (SAM)
- OrderSagaCoordinator: shadow / enforce mode 보존, @value 의 ${...}
  Kotlin string template 충돌 회피 위해 \$ 이스케이프

호환성:
- OrderSagaConfigTests / OrderSagaCoordinatorTests / OrderSagaCoordinatorEnforceTests
  Java caller 무변경 통과
- OrderService.saga.begin(orderId) / .apply(machine, event) / .assertConsistent(...)
  메서드 시그너처 보존
Group 7 — 도메인 의존 정점 (service) + 외부 호출 wiring (config).

config (3):
- PaymentClientProperties / InventoryClientProperties: @JvmRecord data class
  + @ConfigurationProperties
- ClientsConfig: @EnableConfigurationProperties(kotlin class refs) +
  @bean(name = ["paymentLimiter"]) — name 은 Kotlin array literal
  interceptor chain (RetryInterceptor 바깥 / AdaptiveLimiterInterceptor 안쪽) 보존

service (3):
- InventoryClient / PaymentClient: nested @JvmRecord data class (Request/Result)
  + nested OutOfStockException / InventoryInfraException / PaymentInfraException
  (Java caller 가 .OutOfStockException 등 으로 catch — 보존)
  PaymentResult.isSuccess 는 computed property + @get:JvmName("isSuccess")
- OrderService: SAGA shadow + outbox enqueue 흐름 보존, smart-cast 회피를
  위해 orderId 로컬 캐시 (Order.id 는 var 이라 컴파일러가 smart-cast 거부)
  ifPresent { } / executeWithoutResult { } / .map { } 등 람다 화

호환성:
- OrderServiceApplicationTests (IT, Docker) 의 @MockitoBean 대상이 무변경
  - InventoryClient.ReservationResult / OutOfStockException
  - PaymentClient.PaymentResult
- ClientsConfig 의 RestClient bean 이름 (paymentRestClient/inventoryRestClient) 보존
Group 8 — REST 표면 + main application 클래스 (마지막 그룹).

web/dto (4):
- CreateOrderItemRequest / CreateOrderRequest / OrderItemResponse /
  OrderResponse: @JvmRecord data class
  validation 어노테이션은 @field: site target (Kotlin val 의 default 는
  property — JSR-303 validator 는 field 를 읽음)
  OrderResponse / OrderItemResponse 의 companion @JvmStatic from(...) 보존

web (1):
- OrderController: @RestController @RequestMapping("/orders") @validated
  @PostMapping / @GetMapping idiomatic, request body @Valid 보존

main (1):
- OrderServiceApplication: @SpringBootApplication class +
  top-level fun main (Kotlin idiom — Java main 은 합성 KtClass.main 으로 노출)

호환성:
- OrderServiceApplicationTests (IT, Docker 필요) 의 endpoint 호출
  - POST /orders → 201 + OrderResponse
  - GET /orders/{id} → OrderResponse
  - GET /orders?size=N → List<OrderResponse>
  스키마 / JSON shape 보존 (data class 가 record 와 동일 직렬화)

마이그레이션 완료: 50/50 — order-service 100% Kotlin.
8 portfolio service 의 k6 부하 시나리오를 commerce-ops Prometheus 로 흘려보내
한 대시보드에서 client load + server actuator 를 같이 본다.

- prometheus.yml: external_labels + remote-write receiver 사용 가이드 주석
  (docker-compose 의 --web.enable-remote-write-receiver 는 이미 활성).
- infra/grafana/dashboards/portfolio-load.json (uid=portfolio-load):
  * service / scenario / actuator 3개 변수로 cross-service 필터
  * k6 조감 (vus / req/s / p95 / p99 / failed / iter / ws msgs)
  * 시나리오 invariant (matched_count / token_issued / tenant_leak
    / queue_depth / billing lock wait / notify ratelimit / search
    cache hit / feed bp drop)
  * 서버 actuator (HikariCP / outbox / Saga 보상 / CPU / heap / http p95)
- README: Load test 절에 8 service remote-write 사용법 추가
  (K6_PROMETHEUS_RW_SERVER_URL + run-load.sh 한 줄). k6 0.42+ 명시.
8 endpoint (list/get/replay/discard/bulk-replay/bulk-discard/bulk-jobs/stats)
on /api/v1/admin/dlq. notification (ADR-0015) / billing (ADR-0033) /
market (ADR-0028) / gpu (ADR-0026) 의 검증된 표준을 commerce-ops 의 첫 service
인 order 에 도입.

설계:
- DlqSource 5 종 (ORDER_EVENT / INVENTORY_INBOX / PAYMENT_INBOX / SAGA / OUTBOX)
- 6 종 DTO (List / Detail / Replay / Discard / Bulk / Stats response)
- 4 종 port (DlqUseCase / AdminRateLimiter / DlqMessageRepository /
  DlqBulkJobRepository / DlqAuditLog)
- 2 종 Service (DlqAdminService / DlqBulkJobService)
- 어댑터: RedisAdminRateLimiter (in-memory fallback) /
  InMemoryDlqBulkJobRepository / InMemoryDlqMessageRepository /
  Slf4jDlqAuditLog (Loki MDC audit="true")

order 특유:
- byOrder / byCustomer stats 차원
- SAGA / OUTBOX replay 의 멱등성 (ADR-023 의 handleFailure 회계 활용)
- INVENTORY_INBOX / PAYMENT_INBOX replay 는 dedup skip (UNIQUE 충돌 차단)

안전:
- X-Admin-Role 헤더 (DLQ_ADMIN / PLATFORM_ADMIN) — 1 차 게이트
- bulk source 필수, confirm=false 면 항상 dry-run, dry-run 결과는 실 실행과 같은 모양
- bulk-discard 는 hard DELETE 차단 (soft delete + retention)
- rate limit scope (dlq.read=60 / write=30 / bulk=5 per minute)
- audit DLQ_REPLAY / DLQ_DISCARD / DLQ_BULK_* — actor / target / reason / 결과

테스트 (Kotlin slice, Docker 불필요):
- DlqAdminServiceTests — list / replay / discard / bulk dry-run / bulk execute / stats 차원
- InMemoryDlqMessageRepositoryTests — cursor / match / stats bucket 정렬
- RedisAdminRateLimiterTests — scope 별 한도, throttle, key 격리

deps: OrderServiceApplicationTests 의 Mockito any() → anyInt() — 21365ef 의
Kotlin 마이그레이션이 InventoryClient.reserve(... Int) 로 바꿨는데 테스트는
any() 그대로 둬 NPE. 본 step 의 `./gradlew check` 통과 조건이라 같이 fix.

docs:
- ADR-026 — DLQ admin REST API 표준 v2 (3 service 확산)
- services/order-service/README.md 의 DLQ admin 절 (curl 8 예시)

빌드: ./gradlew check BUILD SUCCESSFUL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 endpoint on /api/v1/admin/dlq. order-service 와 *같은 모양* — 콘솔이 8 종
클라이언트 코드를 1 종으로 유지. payment 특유 차이는 의도된 확장점.

payment 특유:
- DlqSource = PAYMENT_CHARGE / PAYMENT_REFUND / PG_CALLBACK / OUTBOX
- stats 차원 byCustomer
- replay 시 PG 의 Idempotency-Key 헤더를 그대로 복사 — billing 패턴.
  같은 키로 PG 가 같은 결제로 인식해 두 번 차감 차단. 응답 idempotencyKey
  필드에 사용한 키를 노출 (PG audit 매칭).
- PAYMENT_REFUND 액션은 audit 로그에 risk=high 표식 — 돈 *돌려주는* 동작이라
  reviewer 가 더 신중하게 결정하도록.
- PG_CALLBACK 의 replay 는 idempotencyKey=null (webhook 의 서명/payload 재검증)

공통 안전 (ADR-026):
- X-Admin-Role / X-Actor 헤더
- bulk source 필수, confirm=false 면 dry-run 강제, hard DELETE 차단
- rate limit scope dlq.read=60 / write=30 / bulk=5 per minute
- audit DLQ_REPLAY / DLQ_DISCARD / DLQ_BULK_* — Loki MDC audit="true"

테스트 (Kotlin slice, Docker 불필요):
- Idempotency-Key 복사 / deterministic key 합성 / PG_CALLBACK 의 null key
- PAYMENT_REFUND 의 risk=high audit 표식
- bulk dry-run 강제, blank reason 거절
- byCustomer stats 차원
- rate limiter scope 독립 카운터

빌드: ./gradlew check BUILD SUCCESSFUL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 endpoint on /api/v1/admin/dlq. order / payment 와 *같은 모양* — commerce-ops
3 service 의 마지막 확산. notification (ADR-0015) / billing (ADR-0033) /
market (ADR-0028) / gpu (ADR-0026) 의 검증된 표준 합집합.

inventory 특유:
- DlqSource = RESERVE_FAILED / RELEASE_FAILED / KAFKA_CONSUME / OUTBOX
- stats 차원 byProduct + bySku (변종/색상 단위 분리)
- replay 시 *Redisson 분산락 재획득* — `product:{productId}` 키로 정상
  reserve/release 와 같은 prefix + 같은 wait/lease. 락 timeout 이면 응답
  lockAcquired=false + reason=LOCK_TIMEOUT 으로 콘솔이 즉시 인지. 락 없이
  재처리하면 재고가 음수가 되는 동시성 사고 — 본 service 의 replay 가 가장
  위험한 작업이라 lock 결과를 응답에 반드시 노출.
- KAFKA_CONSUME / OUTBOX 의 replay 는 락 대상 아님 (consumer 자체 멱등 /
  outbox 는 발행 자체)
- RedissonClient 가 등록되어 있을 때만 RedissonDlqDistributedLock 활성화 —
  fallback NoOpDlqDistributedLock 은 단순 통과 (운영에선 Redisson 필수)

공통 안전 (ADR-026):
- X-Admin-Role / X-Actor 헤더
- bulk source 필수, confirm=false 면 dry-run 강제, hard DELETE 차단
- rate limit scope dlq.read=60 / write=30 / bulk=5 per minute
- audit DLQ_REPLAY / DLQ_DISCARD / DLQ_BULK_* — Loki MDC audit="true"

테스트 (Kotlin slice, Docker 불필요):
- RESERVE_FAILED replay 가 product:{productId} 락 재획득
- 락 timeout 시 LOCK_TIMEOUT 응답 + lockAcquired=false
- KAFKA_CONSUME / OUTBOX 는 락 비대상
- 멱등성 (같은 idempotencyKey 의 두 번째 호출은 같은 응답)
- bulk dry-run 강제 / blank reason 거절
- byProduct + bySku stats 차원

docs:
- top README 에 3 service cross-link + 표준 안전 항목 + 차이 표 (port / source /
  차원 / replay 특유)

빌드: ./gradlew check BUILD SUCCESSFUL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
springdoc-openapi-starter-webmvc-ui 의존 추가 + openapi-gradle-plugin
적용. generateOpenApiDocs 가 docs/openapi/order-service.yaml 을
생성한다. 외부 참조 / SDK codegen 의 단일 진실값. 앱 부팅에 인프라가
필요하므로 실제 yaml 은 CI 에서 생성한다 (docs/openapi/README.md 참고).
springdoc-openapi-starter-webmvc-ui 의존 추가 + openapi-gradle-plugin
적용. generateOpenApiDocs 가 docs/openapi/payment-service.yaml 을
생성한다. 외부 참조 / SDK codegen 의 단일 진실값. 앱 부팅에 인프라가
필요하므로 실제 yaml 은 CI 에서 생성한다 (docs/openapi/README.md 참고).
springdoc-openapi-starter-webmvc-ui 의존 추가 + openapi-gradle-plugin
적용. generateOpenApiDocs 가 docs/openapi/inventory-service.yaml 을
생성한다. 외부 참조 / SDK codegen 의 단일 진실값. 앱 부팅에 인프라가
필요하므로 실제 yaml 은 CI 에서 생성한다 (docs/openapi/README.md 참고).
Bumps [org.springframework.boot](https://github.com/spring-projects/spring-boot) from 3.5.14 to 4.0.6.
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](spring-projects/spring-boot@v3.5.14...v4.0.6)

---
updated-dependencies:
- dependency-name: org.springframework.boot
  dependency-version: 4.0.6
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot force-pushed the dependabot/gradle/services/payment-service/org.springframework.boot-4.0.6 branch from bad8fb0 to 6a4903c Compare May 21, 2026 03:00
@ssa1004 ssa1004 closed this May 21, 2026
@dependabot @github
Copy link
Copy Markdown
Author

dependabot Bot commented on behalf of github May 21, 2026

OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting @dependabot ignore this major version or @dependabot ignore this minor version. You can also ignore all major, minor, or patch releases for a dependency by adding an ignore condition with the desired update_types to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

@dependabot dependabot Bot deleted the dependabot/gradle/services/payment-service/org.springframework.boot-4.0.6 branch May 21, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant