Skip to content

Development Conventions

Coding standards and patterns extracted from the CLAUDE.md files in the backend and frontend repositories.

Backend (../schemastack-be)

Naming

ContextConventionExample
Java classesPascalCaseViewColumnService
Java methods/variablescamelCasefindByUuid
Database tables/columnssnake_caseview_column
REST endpointskebab-case/api/workspace-members
Documentation fileslowercase-with-dashessystem-architecture.md

API Design — UUIDs Only

  • REST API responses must use UUIDs, never internal numeric IDs (Long id)
  • DTOs exposed to the frontend have a uuid field (UUID), not an id field (Long)
  • Internal numeric IDs are implementation details that must never leak to the frontend
  • Path parameters: /api/constraints/{constraintUuid}
  • Request bodies reference related entities by UUID: "columnUuid": "a1b2c3d4-..."

ViewColumn Architecture

ViewColumn uses composition, not inheritance:

java
@Entity
@Table(name = "view_column")
public class ViewColumn extends PanacheEntity {
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "column_id", nullable = false)
    private ColumnMetadata column;  // HAS-A relationship, not IS-A
}
  • Schema import: ColumnMetadata is created FIRST, then ViewColumn references it
  • ViewColumn properties: UI-specific (displayName, widgetType, visible, sortable, readonly)
  • ColumnMetadata properties: Database column properties (name, type, nullable, unique, length)
  • Delegating getters on ViewColumn forward to column.getXxx() for backwards compatibility

Reactive Programming Rules

  • All database operations use Hibernate Reactive (Mutiny)
  • Return Uni<T> for single results, Multi<T> for streams
  • Use @WithTransaction for transactional methods
  • Never fetch lazy collections in parallel — causes session corruption. Use sequential chains or JOIN FETCH
  • See Patterns & Known Issues for HR000069 solutions

Nested Session Prevention

The #1 cause of HR000069 errors is nested sessions:

  1. Service manages the entire lifecycle — endpoint just calls the service, doesn't open sessions
  2. Use JOIN FETCH to eagerly load ALL associations
  3. Extract entity data to primitives before thread switching
  4. .call() before .flatMap() forces session to close
  5. Panache.withSession() ends before the .flatMap() with thread switch
  6. .runSubscriptionOn() only runs after session is closed

JBoss Logger — Avoid Method Ambiguity

JBoss Logger has overloading ambiguity with wrapper types (Long, Integer):

java
// BAD — ambiguous
LOG.debugf("Processing entity %d with %d fields", entityId, data.size());

// GOOD — use %s for wrapper types
LOG.debugf("Processing entity %s with %s fields", entityId, data.size());

When mixing wrapper types with primitives, cast the wrapper to Object:

java
LOG.debugf("Entity %s with %s fields", (Object) entityId, data.size());

Database Migrations

  • Always use Liquibase for schema changes in Quarkus modules
  • Changesets must be idempotent and reversible
  • Never modify existing changesets — create new ones
  • Version format: changeset-{module}-{version}-{description}.xml

Message Contracts

  • All RabbitMQ messages defined in messaging-contracts module
  • Must be framework-agnostic (no Quarkus/Spring dependencies)
  • Use Jackson for JSON serialization
  • Include builder pattern with Lombok

Exception Handling in Reactive Chains

Let JAX-RS exceptions (WebApplicationException, ForbiddenException, NotFoundException) bubble up to exception mappers. Use predicates to exclude them:

java
return service.doSomething()
    .onFailure(NotFoundException.class).recoverWithItem(...)
    .onFailure(t -> !(t instanceof WebApplicationException)).recoverWithItem(...);

Running Tests

Maven skips tests by default. Always pass -DskipTests=false:

bash
cd quarkus/metadata/metadata-test
../../../mvnw test -DskipTests=false

# Specific test
../../../mvnw test -Dtest=SchemaDuplicateColumnTest -DskipTests=false

Module Dependencies

  • Organisation module is intentionally standalone and reusable — minimal dependencies on metadata
  • Organisation → can be extracted for future SaaS projects
  • Metadata → CAN depend on organisation (users, orgs, workspaces, permissions)
  • No circular dependencies between modules

Frontend (../schemastack-fe)

Critical Rules

  1. Flag backend contract mismatches rather than hacking the frontend to work around them. The app is pre-launch so the backend should be fixed.
  2. No component-level style encapsulation — ALL styles go in src/styles/ folder, never use styleUrl or styles in component decorators. Scope by prefixing selectors with component tag names.

Optimistic Updates with SSE Pattern

All CRUD operations that can be synced with other users follow this pattern:

  1. Component dispatches BOTH local and API actions
  2. Reducer handles local action immediately (optimistic update)
  3. Effect handles API call and returns success/failure
  4. SSE broadcasts update to other users (filtered by userId)
  5. Other clients receive SSE event and apply update

Critical rules:

  • Always get state BEFORE local reorder using take(1)
  • Calculate position from original state, not after the local reorder
  • Dispatch local action first (instant feedback), then API action (persistence)
  • Use exhaustMap in effects when actions can fire rapidly
  • Always filter SSE by userId to prevent echo loops
  • Positions are 0-based

Sidenav Pages — Prevent Layout Shift

Pages with sidenav MUST call sidenavService.setMenuItems() in the constructor, NOT in ngOnInit(). Setting items in ngOnInit() causes visible flicker as the template conditionally renders the sidenav container.

Color Styling Policy

  • Never use Tailwind for colors (no bg-blue-500, text-red-600)
  • Always use Material Design 3 tokens via mat.theme-overrides() and component override mixins
  • Custom utility classes (.bg-primary, .text-muted-foreground) map to Material tokens
  • This ensures a single color system with automatic theme switching

Typography Policy

  • Never use inline style="" for font properties
  • Check M3 typography classes first (.mat-label-small, .mat-body-medium, .mat-title-small, etc.)
  • Custom type scale lives in shared/styles/_typography.scss
  • For font-weight: 600, use .font-semibold (M3 only provides 400/500/700)
  • For mat-icon sizing, use .icon-sm/.icon-md/.icon-lg/.icon-xl

M3 Styling Priority System

Follow this exact priority order when styling Material components:

PriorityMethodWhen to Use
1mat.theme()Initial project setup — global color scheme and typography
2mat.theme-overrides()System-level token overrides affecting all components
2.5Component override mixinsFine-grained per-component-type control (mat.card-overrides(), mat.button-overrides(), etc.)
3--mat-component-* variablesComponent-specific CSS variable overrides
4Tailwind classesLayout and structural styling only (flex, grid, spacing, positioning)
5Custom SCSS/CSSLast resort — unique animations, filters, complex combinations

Finding the right variable: DevTools → Inspect element → Computed tab → filter --mat-.

Reusability-First Styling

Before writing new styles:

  1. Check src/styles/_utilities.scss for existing utilities
  2. If it exists, use it
  3. If not, determine if it's reusable (used in 2+ places?) → create utility in _utilities.scss
  4. Only truly component-unique styles go in src/styles/components/
  5. Use semantic naming (.status-badge not .workspace-member-status)

Path Aliases

Use @app/* to reference files from src/app/:

typescript
import { AuthService } from '@app/auth/services/auth.service';
import { AppState } from '@app/store';

HTTP Interceptors (order matters)

  1. authInterceptorFn — adds JWT token
  2. loggingInterceptorFn — logs requests/responses
  3. loadingBarInterceptorFn — controls loading bar UI
  4. loadingInterceptor — manages global loading state

Proxy Configuration

  • /apihttp://localhost:8080 (Quarkus backend)
  • /ssehttp://localhost:8081 (SSE events)

SchemaStack Internal Developer Documentation