# evidence 257 — messaging-msg015-two-kafka-stacks
# revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
# cwd: /shared/codebase/clean-architecture-backend-template/src/messaging
# command: echo ---app-bootstrap seam---; sed -n "36,40p" ../app-bootstrap/src/main/java/dev/caskeleton/bootstrap/messaging/KafkaSenderConfig.java; sed -n "58,66p" ../app-bootstrap/src/main/java/dev/caskeleton/bootstrap/messaging/KafkaSenderConfig.java; echo; echo ---platform producer---; sed -n "124,136p" messaging-spring-boot-starter/src/main/java/dev/caskeleton/messaging/autoconfigure/KafkaMessagingAutoConfiguration.java; echo; echo ---root master switch---; grep -n "ConditionalOnProperty" messaging-spring-boot-starter/src/main/java/dev/caskeleton/messaging/autoconfigure/MessagingPlatformRootAutoConfiguration.java; echo; echo ---CLAUDE.md MSG-015---; grep -n -B2 -A10 "MSG-015" CLAUDE.md
# ---- raw output ----
---app-bootstrap seam---
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "app.messaging.broker", havingValue = "kafka")
public class KafkaSenderConfig {

   * @param settings the broker addresses, already validated as non-empty when Kafka is selected
   * @return the producer, closed on context shutdown
   */
  @Bean(name = "kafkaSeamProducer", destroyMethod = "close")
  @ConditionalOnMissingBean(name = "kafkaSeamProducer")
  public Producer<String, String> kafkaSeamProducer(KafkaAdapterSettings settings) {
    Map<String, Object> config = new HashMap<>();
    config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, String.join(",", settings.brokers()));
    config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

---platform producer---
   *
   * @param environment the resolved environment
   * @return the producer, closed with the context
   */
  @Bean(destroyMethod = "close")
  @ConditionalOnMissingBean
  public org.apache.kafka.clients.producer.Producer<byte[], byte[]> messagingKafkaProducer(
      org.springframework.core.env.Environment environment) {
    String bootstrapServers = environment.getProperty("spring.kafka.bootstrap-servers", "").trim();
    if (bootstrapServers.isEmpty()) {
      throw new IllegalStateException(
          "app.messaging.broker=kafka needs spring.kafka.bootstrap-servers; a producer with no "
              + "broker address defaults to localhost:9092 and fails at the first publish rather "

---root master switch---
4:import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
28:@ConditionalOnProperty(prefix = MessagingSettings.PREFIX, name = "enabled", havingValue = "true")

---CLAUDE.md MSG-015---
68-  증명되었는가", membership은 "무엇이 실행되는가"다.
69-
70:## application이 이 family에 도달하는 경로 (MSG-015, 미해결)
71-
72-리뷰(`docs/reviews/2026-08-14-messaging-module-code-review.md` §6.1·§6.3)가 정한 흐름은
73-`application-owned port → platform anti-corruption bridge → platform`이고, §6.2는 그 bridge의 자리를
74-`adapter/outbound/messaging/platformbridge/`로 지정한다. §6.3 첫 규칙: **application은 신규
75-broker/runtime 타입을 직접 보지 않고 자기 port만 소유한다.**
76-
77-**현재 그 bridge가 없다.** `adapter/outbound/messaging`은 platform에 의존하지 않고(registry의
78-`allowed_dependencies` 참조), `app-bootstrap`이 starter를 직접 문다. 그래서 서로를 모르는 messaging
79-스택 두 개가 한 아티팩트에 있고, 그것이 `kafkaSeamProducer`/`messagingKafkaProducer` bean 이름 충돌로
80-드러났다.
81-
82:이것이 리뷰의 **MSG-015 (P0)** 이며 닫히지 않았다. 지금 안전한 이유는 설계가 아니라 기본값이다 —
83-`app.messaging.enabled=false`. 이 스위치를 켜는 배포는 port를 거치지 않는 경로로 platform을 쓰게 된다.
84-bridge 구현은 런타임 배선 변경이라 리뷰가 말한 대로 별도 change set이다.
85-
86-## Stable 승격에 필요한 증거
87-
88-adapter를 Experimental → Stable로 올릴 때 필요한 것 (`docs/messaging/support-matrix.md`가 현재
89-상태를 기록한다):
90-
91-1. 실제 broker container를 쓰는 fault 시나리오 lane이 존재하고, 그 lane이
92-   `BrokerCertificationEvidence`를 산출한다.
