Story 2.2: Event Management Service - Architecture Compliance Refactoring

Last updated: December 4, 2025 Reading time: 18 minutes File size: 28.87 KB

Story 2.2: Event Management Service - Architecture Compliance Refactoring

Status

Done

Story

As an architect, I want the Event Management Service to comply with all architecture standards from ADR-001, OpenAPI Generator patterns, and shared-kernel integration, so that the service is consistent, maintainable, and follows established patterns across the platform.

Domain Context

Primary Domain

Event Management Domain - Architecture compliance refactoring of existing service (Story 1.15a.1)

Involved Services

  • Event Management Service (refactoring) - Java 21 + Spring Boot 3.2
  • Shared-Kernel (integration) - Exception hierarchy, DomainEvent, DomainEventPublisher, ErrorResponse

IMPORTANT: This is a REFACTORING STORY. Story 1.15a.1 already implemented the Event Management Service with all CRUD operations, filtering, caching, and 51 passing tests. This story brings that implementation to full architecture compliance.

Cross-Domain Dependencies

  • Depends on: Story 1.15a.1 (Events API Consolidation) - DONE
  • Depends on: Shared-Kernel (exception hierarchy, domain events, query utilities)
  • Depends on: Company Management Service architecture compliance patterns (Story 2.1)

Requirements Context

  • NFR-Architecture: All services must follow shared-kernel patterns and OpenAPI Generator approach
  • NFR-Maintainability: Consistent patterns across services for easier maintenance
  • NFR-Observability: Domain events with full metadata for distributed tracing

Acceptance Criteria Source

Epic 2 (docs/prd/epic-2-entity-crud-domain-services.md), Story 2.2, Lines 208-237 (Architecture Compliance Requirements)

Architecture Context

Architecture Patterns

Current State (Story 1.15a.1 Implementation):

  • βœ… Event entity with UUID id, basic CRUD operations
  • βœ… EventController with all endpoints (list, get, create, update, delete, publish, workflow)
  • βœ… Caffeine caching (15min TTL)
  • βœ… GlobalExceptionHandler with custom exceptions
  • βœ… FilterParser, SortParser, PaginationUtils integration
  • βœ… 51 integration tests passing
  • βœ… OpenAPI spec exists (docs/api/events-api.openapi.yml)

Architecture Compliance Gaps (to fix in this story):

  • ❌ Event entity uses UUID as public ID (should use eventCode pattern like β€œBATbern56”)
  • ❌ Manual DTOs (should use OpenAPI Generator)
  • ❌ Custom exceptions don’t fully extend shared-kernel hierarchy
  • ❌ No DomainEventPublisher from shared-kernel (likely custom event publishing)
  • ❌ Domain events may not extend DomainEvent with full metadata
  • ❌ OpenAPI spec defines ErrorResponse/PaginationMetadata locally (should import from shared-kernel)
  • ❌ No schema validation test against architecture

[Source: docs/architecture/ADR-001-invitation-based-user-registration.md]

  • Database-centric architecture, PostgreSQL as single source of truth
  • NO AWS SDK calls for entity CRUD operations

[Source: services/company-user-management-service/OPENAPI-CODEGEN.md]

  • OpenAPI Generator with hybrid approach (generate interfaces/DTOs, write controllers)
  • Import shared-kernel types via importMappings

[Source: services/company-user-management-service/ARCHITECTURE-COMPLIANCE-FIXES.md]

  • Exception hierarchy: extend shared-kernel NotFoundException, ValidationException
  • ErrorResponse: use ch.batbern.shared.dto.ErrorResponse with correlationId, path, severity
  • Domain events: extend DomainEvent with eventId, correlationId, causationId, userId, metadata
  • Event publisher: use ch.batbern.shared.events.DomainEventPublisher (NO custom publishers)

Database Schema Reference

Architecture Schema (Source of Truth):

  • Doc: docs/architecture/03-data-architecture.md, Section β€œEvent” (Lines 367-516)
  • Key Requirement: eventCode as public identifier (e.g., β€œBATbern56”), internal UUID for database

Migration Check (Dev Agent completes in Task 0):

  • Existing Migration: [To be checked - does it match architecture?]
  • Status: [To be filled: βœ… Matches | ⚠️ Needs correction | ❌ Create new]
  • Action: [To be filled by dev agent in Task 0]

Acceptance Criteria

Note: These are REFACTORING criteria, NOT new features. Story 1.15a.1 already implemented the functionality.

  1. OpenAPI Generator Integration - Configure build.gradle with openApiGenerate task
  2. OpenAPI Spec Compliance - Update events-api.openapi.yml to import shared-kernel ErrorResponse and PaginationMetadata (NOT define locally)
  3. Meaningful ID Migration - Migrate Event entity from UUID public ID to eventCode pattern (e.g., β€œBATbern56”)
  4. Exception Hierarchy Compliance - EventNotFoundException extends shared-kernel NotFoundException, BusinessValidationException extends shared-kernel ValidationException
  5. ErrorResponse Compliance - GlobalExceptionHandler uses shared-kernel ErrorResponse with correlationId, path, severity, details
  6. Domain Event Compliance - All domain events extend DomainEvent with full metadata (eventId, correlationId, causationId, userId, metadata)
  7. DomainEventPublisher Integration - Replace custom event publishing with shared-kernel DomainEventPublisher
  8. Schema Validation Test - Create SchemaValidationTest to verify Event entity matches architecture exactly
  9. Database Migration - Update migration to match architecture schema (eventCode, eventNumber)
  10. Integration Tests Updated - All 51 tests still passing after refactoring
  11. No Functional Regressions - All Story 1.15a.1 functionality still works (CRUD, filtering, caching, resource expansion, etc.)

Test Specifications (TDD)

Test Scenarios by Acceptance Criteria

IMPORTANT: This is refactoring, so we’re updating EXISTING tests, not writing all new tests from scratch. Story 1.15a.1 has 51 passing tests that must continue to pass.

AC1-2 Tests: OpenAPI Generator & Spec Compliance

  • Test 1.1: should_generateAPIsFromSpec_when_openApiGenerateRuns
  • Test 1.2: should_importErrorResponseFromSharedKernel_when_specParsed (verify NOT locally defined)
  • Test 1.3: should_importPaginationMetadataFromSharedKernel_when_specParsed (verify NOT locally defined)

AC3 Tests: Meaningful ID Migration (eventCode)

  • Test 3.1: should_useEventCode_when_apiResponseReturned (NOT UUID)
  • Test 3.2: should_generateEventCode_when_eventCreated (from eventNumber)
  • Test 3.3: should_getEventByCode_when_codeProvided (e.g., β€œBATbern56”)
  • Test 3.4: should_return404_when_invalidEventCodeProvided

AC4-5 Tests: Exception & ErrorResponse Compliance

  • Test 4.1: should_extendNotFoundException_when_eventNotFound
  • Test 4.2: should_extendValidationException_when_businessValidationFails
  • Test 5.1: should_includeCorrelationId_when_errorResponseReturned
  • Test 5.2: should_includePath_when_errorResponseReturned
  • Test 5.3: should_includeSeverity_when_errorResponseReturned

AC6-7 Tests: Domain Event & Publisher Compliance

  • Test 6.1: should_extendDomainEventUUID_when_eventPublished (DomainEvent)
  • Test 6.2: should_includeEventMetadata_when_domainEventCreated (eventId, correlationId, causationId, userId)
  • Test 7.1: should_useDomainEventPublisher_when_eventCreated (shared-kernel, NOT custom)

AC8-9 Tests: Schema Validation

  • Test 8.1: should_matchArchitecture_when_entityMapped (SchemaValidationTest)
  • Test 9.1: should_haveEventCodeColumn_when_migrationApplied
  • Test 9.2: should_haveEventNumberColumn_when_migrationApplied

AC10-11 Tests: Regression Testing

  • Test 10.1: should_pass51Tests_when_refactoringComplete (all Story 1.15a.1 tests)
  • Test 11.1: should_performCRUD_when_endToEndFlow (functional regression)

Test File Locations

Existing Test Files (to update):

  • services/event-management-service/src/test/java/ch/batbern/events/controller/EventControllerIntegrationTest.java (51 tests)
  • services/event-management-service/src/test/java/ch/batbern/events/service/EventServiceTest.java

New Test Files (to create):

  • services/event-management-service/src/test/java/ch/batbern/events/validation/SchemaValidationTest.java (schema validation)
  • services/event-management-service/src/test/java/ch/batbern/events/openapi/OpenAPIGeneratorTest.java (verify spec compliance)

Test Data & Mocks

Use existing test data from Story 1.15a.1:

  • EventTestDataBuilder
  • TestSecurityConfig
  • Testcontainers PostgreSQL

Tasks / Subtasks (Refactoring Workflow)

  • Task 0: Assessment & Planning

  • Task 1: OpenAPI Spec Compliance (AC: 2)

    • Read docs/api/events-api.openapi.yml
  • Task 2: OpenAPI Generator Configuration (AC: 1)

    • Update build.gradle
    • Run
    • Verify generated interfaces in
  • Task 3: Exception Hierarchy Refactoring (AC: 4, 5)

  • Task 4: Domain Event Refactoring (AC: 6)

    • Update all events to extend DomainEvent
  • Task 5: DomainEventPublisher Integration (AC: 7)

  • Task 6: Meaningful ID Migration (AC: 3, 9)

    • Database First
    • Entity Next
    • Repository Next
    • Controller/Service Next
    • Tests Next
  • Task 7: Schema Validation Test (AC: 8)

  • Task 8: Regression Testing (AC: 10, 11)

    • Run full test suite:
  • Task 9: Documentation Updates

  • Task 10: Final Verification

Dev Notes - Implementation Guide

Current Implementation (Story 1.15a.1)

Files to Refactor:

services/event-management-service/src/main/java/ch/batbern/events/
β”œβ”€β”€ domain/
β”‚   β”œβ”€β”€ Event.java                    # ❌ Uses UUID as public ID β†’ needs eventCode
β”‚   β”œβ”€β”€ Session.java                   # βœ… OK (sub-resource)
β”‚   └── Registration.java              # βœ… OK (sub-resource)
β”œβ”€β”€ repository/
β”‚   β”œβ”€β”€ EventRepository.java          # ⚠️ Add findByEventCode()
β”‚   β”œβ”€β”€ SessionRepository.java         # βœ… OK
β”‚   └── RegistrationRepository.java    # βœ… OK
β”œβ”€β”€ service/
β”‚   β”œβ”€β”€ EventSearchService.java        # ⚠️ Update to use eventCode
β”‚   └── EventAnalyticsService.java     # βœ… OK
β”œβ”€β”€ controller/
β”‚   β”œβ”€β”€ EventController.java          # ❌ Uses UUID in path β†’ needs eventCode
β”‚   β”œβ”€β”€ SessionController.java         # βœ… OK
β”‚   └── RegistrationController.java    # βœ… OK
β”œβ”€β”€ dto/
β”‚   β”œβ”€β”€ EventResponse.java             # ❌ Manual DTO β†’ should be generated
β”‚   β”œβ”€β”€ CreateEventRequest.java        # ❌ Manual DTO β†’ should be generated
β”‚   β”œβ”€β”€ UpdateEventRequest.java        # ❌ Manual DTO β†’ should be generated
β”‚   └── PatchEventRequest.java         # ❌ Manual DTO β†’ should be generated
β”œβ”€β”€ exception/
β”‚   β”œβ”€β”€ EventNotFoundException.java    # ❌ Extends RuntimeException β†’ should extend shared-kernel NotFoundException
β”‚   β”œβ”€β”€ BusinessValidationException.java # ❌ Extends RuntimeException β†’ should extend shared-kernel ValidationException
β”‚   β”œβ”€β”€ WorkflowException.java         # ❌ Extends RuntimeException β†’ should extend shared-kernel ValidationException
β”‚   └── GlobalExceptionHandler.java    # ❌ Uses local ErrorResponse β†’ should use shared-kernel ErrorResponse
β”œβ”€β”€ event/
β”‚   β”œβ”€β”€ EventCreatedEvent.java         # ❌ Likely custom POJO β†’ should extend DomainEvent<UUID>
β”‚   β”œβ”€β”€ EventUpdatedEvent.java         # ❌ Likely custom POJO β†’ should extend DomainEvent<UUID>
β”‚   β”œβ”€β”€ EventPublishedEvent.java       # ❌ Likely custom POJO β†’ should extend DomainEvent<UUID>
β”‚   └── EventCancelledEvent.java       # ❌ Likely custom POJO β†’ should extend DomainEvent<UUID>
└── config/
    β”œβ”€β”€ CacheConfig.java               # βœ… OK (Caffeine)
    └── TestSecurityConfig.java        # βœ… OK

Refactoring Patterns

Pattern 1: Exception Hierarchy (from company-user-management-service)

java
// Before (Story 1.15a.1)
public class EventNotFoundException extends RuntimeException {
    public EventNotFoundException(String eventId) {
        super("Event not found with ID: " + eventId);
    }
}

// After (Story 2.2 - Architecture Compliant)
public class EventNotFoundException extends NotFoundException {
    public EventNotFoundException(String eventCode) {
        super("Event", eventCode);  // Uses shared-kernel constructor
    }
}

Pattern 2: ErrorResponse (from company-user-management-service)

java
// Before (Story 1.15a.1) - Likely local ErrorResponse
ErrorResponse error = new ErrorResponse(
    "NOT_FOUND",
    "Event not found",
    404
);

// After (Story 2.2 - Architecture Compliant)
ErrorResponse error = ErrorResponse.builder()
    .timestamp(Instant.now())
    .path(request.getRequestURI())              // βœ… New
    .status(HttpStatus.NOT_FOUND.value())
    .error("Not Found")
    .message(ex.getMessage())
    .correlationId(CorrelationIdGenerator.generate())  // βœ… New
    .severity("WARNING")                        // βœ… New
    .build();

Pattern 3: Domain Events (from company-user-management-service)

java
// Before (Story 1.15a.1) - Likely custom POJO
@Data
@Builder
public class EventCreatedEvent {
    private UUID eventId;
    private String title;
    private Instant eventTimestamp;
}

// After (Story 2.2 - Architecture Compliant)
@Getter
@EqualsAndHashCode(callSuper = true)
public class EventCreatedEvent extends DomainEvent<UUID> {
    private final String eventCode;  // Public ID
    private final Integer eventNumber;
    private final String title;
    // ... other fields

    public EventCreatedEvent(
            UUID eventId,           // Internal UUID for DomainEvent
            String eventCode,       // Public eventCode
            Integer eventNumber,
            String title,
            String userId) {        // User from JWT
        super(eventId, "EventCreated", userId);  // DomainEvent<UUID>
        this.eventCode = eventCode;
        this.eventNumber = eventNumber;
        this.title = title;
    }
}

Pattern 4: DomainEventPublisher (from company-user-management-service)

java
// Before (Story 1.15a.1) - Likely custom publisher
@Service
public class EventService {
    private final CustomEventPublisher eventPublisher;  // ❌ Custom

    public void createEvent(CreateEventRequest request) {
        Event saved = eventRepository.save(event);
        eventPublisher.publishEventCreatedEvent(saved);
    }
}

// After (Story 2.2 - Architecture Compliant)
@Service
public class EventService {
    private final DomainEventPublisher eventPublisher;  // βœ… Shared-kernel
    private final SecurityContextHelper securityContextHelper;

    public void createEvent(CreateEventRequest request) {
        Event saved = eventRepository.save(event);

        String userId = securityContextHelper.getCurrentUserId();
        EventCreatedEvent event = new EventCreatedEvent(
            saved.getId(),           // UUID
            saved.getEventCode(),    // Public eventCode
            saved.getEventNumber(),
            saved.getTitle(),
            userId
        );
        eventPublisher.publish(event);  // βœ… Shared-kernel publisher
    }
}

Pattern 5: Meaningful ID (eventCode)

java
// Before (Story 1.15a.1) - UUID as public ID
@Entity
public class Event {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;  // ❌ Exposed in API
    // ...
}

// Controller
@GetMapping("/{eventCode}")
public EventResponse getEvent(@PathVariable String eventCode) {
    return eventService.getEventByCode(eventCode);
}

// After (Story 2.2 - Architecture Compliant)
@Entity
public class Event {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;  // βœ… Internal database key only

    @Column(name = "event_code", unique = true, nullable = false)
    private String eventCode;  // βœ… Public identifier (e.g., "BATbern56")

    @Column(name = "event_number", unique = true, nullable = false)
    private Integer eventNumber;  // βœ… Sequential number

    @PrePersist
    protected void onCreate() {
        if (eventCode == null && eventNumber != null) {
            eventCode = "BATbern" + eventNumber;  // Auto-generate
        }
    }
}

// Controller
@GetMapping("/{eventCode}")
public EventResponse getEvent(@PathVariable String eventCode) {
    return eventService.getEventByCode(eventCode);
}

// Response DTO (id field returns eventCode, NOT UUID)
{
  "id": "BATbern56",  // βœ… eventCode
  "eventNumber": 56,
  "title": "BATbern 2025",
  // ...
}

OpenAPI Spec Updates Required

File: docs/api/events-api.openapi.yml

Remove (if present):

yaml
# Lines ~1438-1452 - Remove local PaginationMetadata definition
PaginationMetadata:
  type: object
  properties:
    page:
      type: integer
    # ...

# Lines ~1454-1466 - Remove local ErrorResponse definition
ErrorResponse:
  type: object
  properties:
    error:
      type: string
    # ...

Add Instead:

yaml
components:
  schemas:
    # Import shared-kernel types via external references
    ErrorResponse:
      $ref: '../../../shared-kernel/src/main/resources/openapi/shared-types.yaml#/components/schemas/ErrorResponse'

    PaginationMetadata:
      $ref: '../../../shared-kernel/src/main/resources/openapi/shared-types.yaml#/components/schemas/PaginationMetadata'

Update eventCode pattern:

yaml
paths:
  /api/v1/events/{eventCode}:  # βœ… Use eventCode, NOT {id}
    get:
      parameters:
        - name: eventCode
          in: path
          required: true
          schema:
            type: string
            pattern: '^BATbern[0-9]+$'
            example: 'BATbern56'

Database Migration Update

File: services/event-management-service/src/main/resources/db/migration/VX__update_events_meaningful_id.sql

sql
-- Add eventCode and eventNumber columns
ALTER TABLE events
  ADD COLUMN event_code VARCHAR(50),
  ADD COLUMN event_number INTEGER;

-- Populate eventNumber from existing data (sequential)
UPDATE events
SET event_number = ROW_NUMBER() OVER (ORDER BY created_at);

-- Populate eventCode from eventNumber
UPDATE events
SET event_code = 'BATbern' || event_number;

-- Add constraints
ALTER TABLE events
  ALTER COLUMN event_code SET NOT NULL,
  ALTER COLUMN event_number SET NOT NULL,
  ADD CONSTRAINT uq_event_code UNIQUE (event_code),
  ADD CONSTRAINT uq_event_number UNIQUE (event_number);

-- Create indexes
CREATE UNIQUE INDEX idx_event_code ON events(event_code);
CREATE UNIQUE INDEX idx_event_number ON events(event_number);

-- Create trigger for auto-generation
CREATE OR REPLACE FUNCTION generate_event_code()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.event_code IS NULL AND NEW.event_number IS NOT NULL THEN
        NEW.event_code := 'BATbern' || NEW.event_number;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_generate_event_code
    BEFORE INSERT ON events
    FOR EACH ROW
    EXECUTE FUNCTION generate_event_code();

Regression Testing Strategy

Critical: All 51 tests from Story 1.15a.1 MUST still pass after refactoring.

Test Update Pattern:

java
// Before (Story 1.15a.1) - UUID assertions
@Test
void should_getEventById_when_eventExists() {
    UUID eventId = UUID.randomUUID();
    // ... create event with UUID

    mockMvc.perform(get("/api/v1/events/" + eventId))
        .andExpect(jsonPath("$.id").value(eventId.toString()));
}

// After (Story 2.2) - eventCode assertions
@Test
void should_getEventByCode_when_eventExists() {
    String eventCode = "BATbern56";
    // ... create event with eventCode

    mockMvc.perform(get("/api/v1/events/" + eventCode))
        .andExpect(jsonPath("$.id").value("BATbern56"));  // Returns eventCode as id
}

Definition of Done Checklist

Development Complete

  • Domain events extend DomainEvent

Schema Validation

Regression Testing

Review Ready

Change Log

Date Version Description Author
2025-10-27 1.0 Story created as REFACTORING story (not greenfield) to bring Story 1.15a.1 to architecture compliance Bob (SM Agent)

Dev Agent Record

Agent Model Used

claude-sonnet-4-5-20250929

Implementation Approach

REFACTORING story - Story 1.15a.1 already implemented most features. This story brings code to full architecture compliance.

Assessment Findings (Task 0 - Completed)

Current State Analysis:

  • βœ… Meaningful ID (AC3, AC9): ALREADY DONE - Event entity has eventCode/eventNumber, controller uses eventCode in paths
  • βœ… Exception Hierarchy (AC4): ALREADY DONE - All exceptions extend shared-kernel (NotFoundException, ValidationException)
  • βœ… ErrorResponse (AC5): ALREADY DONE - GlobalExceptionHandler uses shared-kernel ErrorResponse with correlationId, path, severity
  • βœ… OpenAPI Generator (AC1): ALREADY DONE - build.gradle configured with importMappings
  • ⚠️ OpenAPI Spec (AC2): PARTIALLY DONE - Generator configured but spec defines ErrorResponse/PaginationMetadata locally (lines 1438-1454+)
  • ❌ Domain Events (AC6): NEEDS FIX - Events extend DomainEvent instead of DomainEvent
  • ❌ DomainEventPublisher (AC7): NOT IMPLEMENTED - Events defined but never published
  • ❌ Schema Validation Test (AC8): DOESN’T EXIST - Need to create
  • βœ… Tests: 61 tests exist (more than 51 mentioned)

Architecture Gaps Identified:

  1. OpenAPI spec has local ErrorResponse/PaginationMetadata schemas β†’ Need external $ref to shared-kernel
  2. EventCreatedEvent, EventUpdatedEvent, EventPublishedEvent, EventCancelledEvent extend DomainEvent β†’ Should be DomainEvent
  3. No event publishing in controller/service β†’ Need DomainEventPublisher integration
  4. Missing SchemaValidationTest

Tasks to Complete:

  • Task 1: Fix OpenAPI spec (remove local schemas, add $ref)
  • Task 2: SKIP (OpenAPI Generator already configured)
  • Task 3: SKIP (Exception hierarchy already compliant)
  • Task 4: Fix domain events to extend DomainEvent
  • Task 5: Add DomainEventPublisher integration
  • Task 6: SKIP (Meaningful ID already implemented)
  • Task 7: Create SchemaValidationTest
  • Task 8: Run regression tests (verify 61 tests pass)
  • Task 9: Update documentation
  • Task 10: Final verification

Debug Log References

[To be filled during implementation]

Completion Notes

[To be filled upon completion]

Files Changed

Files to Modify:

  • docs/api/events-api.openapi.yml (remove local schemas, add $ref)
  • services/event-management-service/src/main/java/ch/batbern/events/event/EventCreatedEvent.java (UUID)
  • services/event-management-service/src/main/java/ch/batbern/events/event/EventUpdatedEvent.java (UUID)
  • services/event-management-service/src/main/java/ch/batbern/events/event/EventPublishedEvent.java (UUID)
  • services/event-management-service/src/main/java/ch/batbern/events/event/EventCancelledEvent.java (UUID)
  • services/event-management-service/src/main/java/ch/batbern/events/controller/EventController.java (add event publishing)

Files to Create:

  • services/event-management-service/src/test/java/ch/batbern/events/validation/SchemaValidationTest.java
  • services/event-management-service/ARCHITECTURE-COMPLIANCE-FIXES.md

Deployment Notes

NO DATA MIGRATION needed - eventCode/eventNumber already in database schema from Story 1.15a.1

QA Results

[To be filled by QA agent after review]