ADR-003: Meaningful Identifiers in Public APIs

Status: Accepted

Last updated: December 20, 2025 Reading time: 14 minutes File size: 23.39 KB
  • Story 1.16.1 (Meaningful IDs for Public-Facing URLs)
  • Story 1.16.2 (Eliminate UUIDs from API)

Context

The initial BATbern platform design used UUIDs as primary identifiers exposed in public APIs and URLs. While UUIDs provide excellent technical properties (uniqueness, immutability, non-sequential), they create several problems for user experience and developer experience:

Problems with UUID-Based APIs

  1. Poor User Experience:

    • URLs like /events/f47ac10b-58cc-4372-a567-0e02b2c3d479 are not memorable or shareable
    • Users cannot type or verbally communicate URLs
    • No semantic meaning - impossible to distinguish BATbern #56 from its UUID
  2. Developer Experience Issues:

    • API responses filled with cryptic UUID strings
    • Difficult to debug and trace entities across systems
    • API documentation examples using UUIDs are confusing
    • Manual testing requires copy-pasting UUIDs constantly
  3. SEO and Marketing Impact:

    • Search engines prefer semantic URLs
    • Marketing materials cannot use clean URLs
    • Event codes like β€œBATbern142” are brandable identifiers
  4. Business Requirements:

    • Event organizers refer to events by number (e.g., β€œBATbern #142”)
    • Companies have established short names (e.g., β€œGoogleZH”, β€œUBS”)
    • Users expect profile URLs like /users/john.doe, not UUID paths

Explored Alternatives

  1. Keep UUIDs in APIs, Use Slugs Only in Frontend Routes

    • Rejected: Requires frontend to maintain UUID/slug mappings
    • Creates two-tier identifier system (public vs internal)
    • Still exposes UUIDs in API responses
  2. Use Numeric Sequential IDs

    • Rejected for PKs: Exposes business metrics (event count, user count)
    • Creates security risks (enumeration attacks)
    • Does not solve human-readability for companies/users
  3. Hybrid Approach: Database UUIDs + API Slugs

    • Selected: Provides both technical benefits and UX benefits
    • Aligns with industry best practices (GitHub, Stripe, etc.)

Decision

We have decided to implement a dual-identifier strategy where:

Database Layer (Internal - UUIDs for Own Entities Only)

Within-Service References (Same Microservice):

  • UUID primary keys for all entities owned by this service
  • UUID foreign keys ONLY for relationships within the same service’s database
  • Performance characteristics of UUID PKs preserved:
    • B-tree index efficiency
    • Immutable identifiers (safe for caching)
    • Distributed ID generation (no coordination needed)

Cross-Service References (Different Microservices):

  • NEVER use UUID foreign keys to entities in other services
  • ALWAYS store meaningful IDs (companyName, username, eventCode) for cross-service references
  • NO database foreign key constraints across service boundaries
  • Microservice isolation - each service owns its own database schema

API Layer (Public - Meaningful Identifiers)

  • Meaningful IDs exposed in all public APIs and URLs:

    • Events: eventCode (e.g., β€œBATbern56”)
    • Companies: name (e.g., β€œGoogleZH”)
    • Users: username (e.g., β€œjohn.doe”)
    • Sessions: slug (e.g., β€œblockchain-security-101”)
  • Meaningful IDs are unique alternate keys:

    • Database unique constraints ensure uniqueness
    • B-tree indexes for lookup performance
    • Meaningful IDs are immutable after creation
  • DTOs use only meaningful IDs:

    json
    {
      "id": "BATbern56",
      "title": "Modern Architecture Symposium",
      "organizerId": "john.doe",
      "venueId": "UniBern"
    }
    

Identifier Format Standards

Event Codes

  • Format: BATbern{eventNumber}
  • Example: BATbern142
  • Generation: Auto-generated from sequential event number
  • Pattern: ^BATbern[0-9]+$
  • Rationale: Matches existing business naming convention

Company Names

  • Format: Alphanumeric, 1-12 characters
  • Example: GoogleZH, UBS, Swisscom
  • Generation: User-provided during company creation
  • Pattern: ^[a-zA-Z0-9]{1,12}$
  • Case: Case-insensitive lookups (GoogleZH = googlezh)
  • Rationale: Short, memorable, brandable identifiers

Usernames

  • Format: {firstname}.{lastname}[.{n}]
  • Example: john.doe, peter.mueller.2
  • Generation: Auto-generated from user’s name
  • Pattern: ^[a-z]+\.[a-z]+(\.[0-9]+)?$
  • German Characters: Γ€β†’ae, ΓΆβ†’oe, ΓΌβ†’ue, ΓŸβ†’ss
  • Collision Handling: Append .2, .3, etc. for duplicates
  • Rationale: Professional, predictable, standard format

Session Slugs

  • Format: Slugified session title
  • Example: blockchain-security-101
  • Generation: Auto-generated from session title
  • Scope: Unique within event (same slug allowed in different events)
  • Pattern: Lowercase, hyphens, no special characters
  • Rationale: SEO-friendly, human-readable session URLs

Implementation Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Frontend Layer                      β”‚
β”‚  URLs: /events/BATbern56, /users/john.doe              β”‚
β”‚  API Calls: GET /api/v1/events/BATbern56               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚ Meaningful IDs only
                     β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    API Gateway Layer                     β”‚
β”‚  Routing: /api/v1/events/{eventCode}                   β”‚
β”‚  JWT Validation, Rate Limiting                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚               Service Layer (Controllers)                β”‚
β”‚  @GetMapping("/events/{eventCode}")                     β”‚
β”‚  β†’ eventService.findByEventCode(eventCode)              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             Repository Layer (JPA/JPQL)                  β”‚
β”‚  findByEventCode(String eventCode) β†’ Event entity       β”‚
β”‚  Query: SELECT e FROM Event e WHERE e.eventCode = :codeβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Database Layer (PostgreSQL)             β”‚
β”‚  events table:                                          β”‚
β”‚    - id UUID PRIMARY KEY (internal)                     β”‚
β”‚    - event_code VARCHAR(20) UNIQUE (public API)         β”‚
β”‚    - organizer_id UUID β†’ users.id (internal FK)         β”‚
β”‚    - title, description, etc.                           β”‚
β”‚                                                          β”‚
β”‚  Index: CREATE UNIQUE INDEX ON events(event_code)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

DTO Projection Strategy

To avoid N+1 queries when resolving foreign keys to meaningful IDs, we use JPQL constructor projections:

java
@Query("""
    SELECT new ch.batbern.events.dto.EventResponse(
        e.eventCode,
        e.title,
        u.username
    )
    FROM Event e
    LEFT JOIN User u ON e.organizerId = u.id
    WHERE e.eventCode = :eventCode
    """)
Optional<EventResponse> findEventResponseByCode(@Param("eventCode") String eventCode);

Benefits:

  • Single database query (no N+1 problem)
  • Automatic FK resolution (UUID β†’ username)
  • Type-safe DTO construction
  • Performant (index-backed joins)

Microservice Isolation Rules

CRITICAL: Services MUST store meaningful IDs for cross-service references, NOT UUIDs.

Rule 1: Own Entities Use UUID PKs

java
// βœ… CORRECT: Event entity in event-management-service
@Entity
@Table(name = "events")
public class Event {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;  // βœ… UUID PK for entity owned by this service

    @Column(name = "event_code", nullable = false, unique = true)
    private String eventCode;  // βœ… Meaningful ID for public APIs
}

Rule 2: Cross-Service References Use Meaningful IDs (NOT UUIDs)

java
// βœ… CORRECT: Partner entity in partner-coordination-service
@Entity
@Table(name = "partners")
public class Partner {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;  // βœ… UUID PK for Partner (owned by this service)

    // Cross-service reference to Company (different service)
    @Column(name = "company_name", nullable = false, unique = true, length = 12)
    private String companyName;  // βœ… Meaningful ID, NOT companyId UUID

    // NO: private UUID companyId;  ❌ WRONG - don't use UUID for other service's entity
}

// βœ… CORRECT: PartnerContact entity
@Entity
@Table(name = "partner_contacts")
public class PartnerContact {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;

    @Column(name = "partner_id", nullable = false)
    private UUID partnerId;  // βœ… OK - Partner is in THIS service

    // Cross-service reference to User (different service)
    @Column(name = "username", nullable = false, length = 100)
    private String username;  // βœ… Meaningful ID, NOT userId UUID

    // NO: private UUID userId;  ❌ WRONG - don't use UUID for other service's entity
}

Rule 3: NO Database Foreign Keys Across Services

sql
-- βœ… CORRECT: partner-coordination-service database schema
CREATE TABLE partners (
    id UUID PRIMARY KEY,
    company_name VARCHAR(12) NOT NULL UNIQUE,  -- βœ… Meaningful ID
    -- NO FOREIGN KEY to companies.id  βœ… Correct
);

CREATE TABLE partner_contacts (
    id UUID PRIMARY KEY,
    partner_id UUID NOT NULL REFERENCES partners(id),  -- βœ… OK - same service
    username VARCHAR(100) NOT NULL,  -- βœ… Meaningful ID
    -- NO FOREIGN KEY to users.id  βœ… Correct
);

-- ❌ WRONG: Don't do this
CREATE TABLE partners (
    id UUID PRIMARY KEY,
    company_id UUID NOT NULL,  -- ❌ WRONG - UUID reference
    FOREIGN KEY (company_id) REFERENCES companies(id)  -- ❌ WRONG - cross-service FK
);

Rule 4: Cross-Service Data Access via HTTP APIs

java
// βœ… CORRECT: Partner service accesses Company data via HTTP
@Service
public class PartnerService {
    private final CompanyServiceClient companyServiceClient;
    private final UserServiceClient userServiceClient;

    @Cacheable(value = "partners", key = "#companyName")
    public PartnerResponse getPartner(String companyName) {
        // 1. Get partner from OWN database (uses companyName)
        Partner partner = partnerRepository.findByCompanyName(companyName)
            .orElseThrow(() -> new PartnerNotFoundException(companyName));

        // 2. Enrich with Company data via HTTP call
        CompanyResponse company = companyServiceClient.getCompany(companyName);

        return PartnerResponse.builder()
            .companyName(partner.getCompanyName())
            .companyDisplayName(company.getDisplayName())  // From HTTP call
            .partnershipLevel(partner.getPartnershipLevel())
            .build();
    }
}

// HTTP client with JWT token propagation
@Component
public class CompanyServiceClient {
    @Cacheable(value = "companies", key = "#companyName")
    public CompanyResponse getCompany(String companyName) {
        HttpHeaders headers = createHeadersWithJwtToken();
        HttpEntity<Void> request = new HttpEntity<>(headers);

        return restTemplate.exchange(
            companyServiceUrl + "/api/v1/companies/" + companyName,
            HttpMethod.GET,
            request,
            CompanyResponse.class
        ).getBody();
    }
}

Rule 5: Within-Service Relationships Can Use UUID FKs

java
// βœ… CORRECT: TopicVote entity in partner-coordination-service
@Entity
@Table(name = "topic_votes")
public class TopicVote {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;

    // Within same service - UUID FK is OK
    @Column(name = "partner_id", nullable = false)
    private UUID partnerId;  // βœ… OK - Partner is in THIS service

    // Cross-service reference - use meaningful ID
    @Column(name = "topic_id", nullable = false)
    private String topicId;  // βœ… Meaningful ID to Event Management Service topic
    // NO: private UUID topicUuid;  ❌ WRONG
}
sql
-- Database schema shows the pattern
CREATE TABLE topic_votes (
    id UUID PRIMARY KEY,
    partner_id UUID NOT NULL REFERENCES partners(id),  -- βœ… Within-service FK OK
    topic_id VARCHAR(100) NOT NULL,  -- βœ… Cross-service: meaningful ID
    -- NO FOREIGN KEY to topics.id  βœ… Correct
);

Summary: UUID vs Meaningful ID Decision Tree

Does this field reference an entity?
β”œβ”€ YES β†’ Is the entity in THIS service's database?
β”‚   β”œβ”€ YES β†’ Use UUID FK βœ…
β”‚   β”‚   Example: partner_id UUID REFERENCES partners(id)
β”‚   └─ NO β†’ Use Meaningful ID βœ…
β”‚       Example: company_name VARCHAR(12), username VARCHAR(100)
└─ NO β†’ Use appropriate data type
    Example: vote_value INTEGER, description TEXT

Shared Kernel Services

Created SlugGenerationService in shared-kernel for consistent slug generation:

java
@Service
public class SlugGenerationService {

    public String generateUsername(String firstName, String lastName) {
        String first = slugifyName(firstName);
        String last = slugifyName(lastName);
        return first + "." + last;
    }

    public String ensureUniqueUsername(String base, Function<String, Boolean> exists) {
        if (!exists.apply(base)) return base;

        int suffix = 2;
        while (exists.apply(base + "." + suffix)) {
            suffix++;
        }
        return base + "." + suffix;
    }

    public String slugifySessionTitle(String title) {
        return title.toLowerCase()
            .replaceAll("Γ€", "ae")
            .replaceAll("ΓΆ", "oe")
            .replaceAll("ΓΌ", "ue")
            .replaceAll("ß", "ss")
            .replaceAll("[^a-z0-9]+", "-")
            .replaceAll("^-|-$", "");
    }
}

Database Migrations

Each service required schema migrations to add meaningful ID columns:

Event Management Service (V3, V4):

sql
-- V3__Add_event_code_and_organizer_username.sql
ALTER TABLE events
ADD COLUMN event_code VARCHAR(50) UNIQUE NOT NULL;

ALTER TABLE events
ADD COLUMN organizer_username VARCHAR(100) NOT NULL;

CREATE UNIQUE INDEX idx_events_event_code ON events(event_code);

Company-User Management Service (V3, V10):

sql
-- V3__Create_companies_schema.sql (updated)
ALTER TABLE companies
ADD COLUMN name VARCHAR(12) UNIQUE NOT NULL;

ALTER TABLE companies
ADD COLUMN display_name VARCHAR(255) NOT NULL;

CREATE UNIQUE INDEX idx_companies_name_lower ON companies(LOWER(name));

-- V10__Create_logos_table.sql
-- Logos table references companies by name (not UUID FK)
ALTER TABLE logos
ADD COLUMN entity_name VARCHAR(255) NOT NULL;

Consequences

Positive

  1. Improved User Experience:

    • Shareable URLs: batbern.ch/events/BATbern56
    • Memorable identifiers for marketing and communication
    • Professional appearance (/users/john.doe vs UUID)
  2. Better Developer Experience:

    • Clean, readable API responses
    • Easy debugging and tracing
    • Intuitive API documentation
    • Faster manual testing (no UUID copy-paste)
  3. SEO Benefits:

    • Semantic URLs improve search engine ranking
    • Event codes appear in search results
    • Better social media link previews
  4. Business Alignment:

    • Event codes match physical conference naming (BATbern #142)
    • Company short names match existing branding
    • Username format matches professional standards
  5. Performance Maintained:

    • UUID PKs preserve join performance
    • Unique indexes on meaningful IDs are fast (B-tree)
    • Single-query DTO projections avoid N+1

Negative

  1. Schema Complexity:

    • Dual identifier strategy adds columns
    • More indexes to maintain (storage overhead ~5-10%)
    • Migration complexity for existing data (mitigated: greenfield)
  2. API Backward Compatibility:

    • Breaking change for existing API consumers
    • Mitigation: Accept both UUID and meaningful ID during transition
    • Status: Not applicable (greenfield system)
  3. Collision Handling Required:

    • Username collisions need suffix logic (.2, .3)
    • Company name collisions require user intervention
    • Mitigation: Unique constraints + clear error messages
  4. ID Immutability Constraints:

    • Meaningful IDs must be immutable (used in URLs, external refs)
    • Cannot rename companies or change usernames easily
    • Mitigation: Design UX to set meaningful IDs at creation time
  5. Cross-Service Coordination:

    • User service must provide username to event service
    • Requires consistent slug generation across services
    • Mitigation: SlugGenerationService in shared-kernel

Technical Debt

  1. ID Wrapper Removal:

    • Removed EventId, UserId, CompanyId wrapper classes
    • Simplified domain events to use String directly
    • Benefit: Reduced complexity, cleaner DTOs
    • Risk: Type safety reduced (String vs EventId)
    • Mitigation: OpenAPI schemas enforce format validation
  2. No UUID in Domain Events:

    • Domain events use meaningful IDs (eventCode, username)
    • Event consumers must handle meaningful ID lookups
    • Risk: Event processing slightly more complex
    • Mitigation: Index-backed lookups are fast

Implementation Status

Completed (Stories 1.16.1 + 1.16.2)

Testing Coverage

Integration Tests (PostgreSQL via Testcontainers):

  • Event repository: findByEventCode() tested
  • Company repository: findByNameIgnoreCase() tested
  • User service: Username generation + collision handling tested
  • Session repository: Slug uniqueness within event tested

E2E Tests (Bruno):

  • GET /api/v1/events/{eventCode}
  • GET /api/v1/companies/{companyName}
  • POST /api/v1/files/presigned-url (entity_name parameter)

Frontend Tests (Vitest):

  • React Router with meaningful ID paths
  • API service layer uses meaningful IDs
  • 1277 passing tests (40 skipped for unimplemented backend features)

Alignment with Industry Best Practices

This decision aligns with how leading platforms handle identifiers:

Platform Internal ID Public API ID Example
GitHub Numeric DB ID Repository slug github.com/facebook/react
Stripe Numeric DB ID Prefixed string cus_1234567890
Slack UUID/Numeric Channel name #general, @john.doe
Medium UUID Article slug medium.com/@author/article-title-abc123
BATbern UUID Meaningful ID batbern.ch/events/BATbern56

Alternatives Considered and Rejected

Alternative 1: Natural Keys as Primary Keys

Description: Use meaningful IDs as primary keys directly (no UUID PKs).

Rejected Because:

  • Requires complex update cascade if ID changes (despite immutability goal)
  • Distributed systems harder (need central ID generator)
  • Larger index size (VARCHAR vs UUID)
  • Foreign keys less efficient (string joins vs UUID joins)

Alternative 2: Composite Keys

Description: Use (tenant_id, sequential_id) composite keys.

Rejected Because:

  • Over-engineering for single-tenant system
  • More complex foreign key relationships
  • No tenant concept in BATbern (single organization)

Alternative 3: Hashids/Short UUIDs

Description: Use shortened UUID variants (e.g., Base62 encoded).

Rejected Because:

  • Still not human-meaningful (random characters)
  • Does not match business requirements (event numbers, usernames)
  • Adds encoding/decoding complexity

Future Considerations

API Versioning Strategy

If we need to support multiple ID formats in the future:

GET /api/v1/events/{eventCode}  # Current
GET /api/v2/events/{eventCode}  # Future (if breaking changes needed)

Meaningful ID Evolution

As the system grows:

  1. Event codes: Could evolve to include year (BATbern2026-01)
  2. Company names: Could support vanity URLs (/companies/google β†’ /companies/GoogleZH)
  3. Usernames: Could add display names separate from login names

Performance Monitoring

Monitor these metrics:

  • Lookup performance: UUID vs meaningful ID queries
  • Index size: Storage overhead of dual identifiers
  • Cache hit rates: Meaningful ID β†’ Entity mappings
  • Architecture: docs/architecture/03-data-architecture.md (updated with meaningful ID columns)
  • API Design: docs/architecture/04-api-design.md (URL patterns)
  • Stories:
    • docs/stories/1.16.1-meaningful-ids-public-urls.md
    • docs/stories/1.16.2-eliminate-uuids-from-api.md
  • OpenAPI Specs:
    • docs/api/events-api.openapi.yml
    • docs/api/companies-api.openapi.yml
  • E2E Tests:
    • bruno-tests/events-api/*
    • bruno-tests/companies-api/*
    • bruno-tests/file-upload-api/*

Revision History

Date Version Changes Author
2025-11-02 1.0 Initial ADR creation Winston (Architect Agent)
2025-11-08 1.1 CRITICAL CLARIFICATION: Added comprehensive β€œMicroservice Isolation Rules” section with 5 rules and decision tree. Made it crystal clear that cross-service references MUST use meaningful IDs (companyName, username, eventCode), NOT UUIDs. Updated database layer description to distinguish within-service vs. cross-service references. Claude Code

This ADR documents a foundational architectural decision that affects all APIs, URLs, and external integrations in the BATbern platform. All microservices MUST follow the microservice isolation rules: use meaningful IDs for cross-service references, NEVER UUIDs.