Skip to main content

Architectural Decision Records

This document tracks all significant architectural decisions made during the development of BonardaHR. Each ADR records the context, decision, alternatives considered, rationale, and consequences. The complete backend source record, including expanded implementation details, is maintained in backend/docs/DECISIONS.md.


ADR-001: Configurable Employee Fields Implementation​

Date: 2026-01-28 | Status: Accepted

Context: We needed a flexible way to add custom fields to employee profiles (e.g., payroll information, hobbies, emergency contacts) without requiring code changes or database migrations for each new field.

Decision: Hybrid JSONB model with:

  • Fixed columns for core employee data (first_name, last_name, email, etc.)
  • Separate tables for section definitions (employee_sections, section_fields)
  • JSONB columns for dynamic field values (employee_field_values.value)

Alternatives Considered:

  • EAV Model — Poor query performance, complex joins, lack of type safety (3Ɨ slower)
  • Separate Table Per Section — Requires schema migrations for new sections; not truly flexible
  • Pure JSONB — Loss of relational integrity for core fields; harder to query consistently

Consequences: No schema changes needed for new sections; field validation handled in application layer; GIN indexes maintain query performance.


ADR-002: State Management Strategy​

Date: 2026-01-28 | Status: Accepted

Decision: Hybrid state management approach:

  • Redux Toolkit: Authentication, user profile, permissions (global, infrequent changes)
  • React Query: All server state (employees, projects, time-off, etc.)
  • Context API: Theme, locale, environment settings
  • Local useState: Component-specific UI state

Rationale: Each tool is used for its strengths; follows 2026 React best practices.


ADR-003: Database Hierarchical Structure​

Date: 2026-01-28 | Status: Accepted

Decision: Adjacency list model with self-referencing FK (reports_to_id) combined with PostgreSQL recursive CTEs for hierarchy queries.

Alternatives Considered: Materialized Path (complex maintenance), Nested Sets (complex updates), Closure Table (overkill for 3–5 level HR org charts).

Consequences: Simple data model; recursive queries handled efficiently by PostgreSQL; circular reference prevention required in application layer.


ADR-004: Authentication & Authorization​

Date: 2026-01-28 | Status: Accepted

Decision:

  • Authentication: Microsoft Azure AD OAuth2 for SSO
  • API Tokens: JWT tokens issued after successful OAuth2 authentication
  • Authorization: Role-Based Access Control (RBAC) with resource-action permissions

Architecture:

  1. User authenticates via Microsoft Azure AD
  2. Backend receives OAuth2 token, validates with Microsoft
  3. Backend creates/links an Employee record
  4. Backend generates JWT with user claims and permissions
  5. Frontend stores JWT and uses it for all API requests
  6. Spring Security validates JWT on each request

Alternatives Considered: Session-based auth (not suitable for stateless API), OAuth2 tokens for API (added latency validating with Microsoft on each request), ABAC (overly complex for current requirements).


ADR-005: Database Migration Tool​

Date: 2026-01-28 | Status: Accepted

Decision: Use Flyway for database migrations — version-controlled, plain SQL migrations with automatic application on startup.

Alternatives Considered: Liquibase (more complex than needed), Manual SQL Scripts (no version control or collaboration support).


ADR-006: Frontend Build Tool​

Date: 2026-01-28 | Status: Accepted

Decision: Use Vite as the frontend build tool and dev server.

Rationale: Near-instant HMR, native ESM support, minimal configuration, significantly faster than webpack/CRA for development iteration.


ADR-007: API Design Pattern​

Date: 2026-01-28 | Status: Accepted

Decision: REST principles with:

  • Resource-based URLs (/api/v1/employees)
  • HTTP verbs for actions (GET, POST, PUT, DELETE)
  • JSON request/response bodies
  • Pagination with page + size query params
  • API versioning in URL (/api/v1/)

ADR-008: Monorepo vs Separate Repos​

Date: 2026-01-28 | Status: Accepted (Monorepo)

Decision: Single repository containing both backend/ and frontend/ alongside docs/ and docker/.

Rationale: Single source of truth, coordinated versioning, simpler CI/CD for a single product. Separate repos rejected due to cross-stack coordination overhead.


ADR-009: Employee Source of Truth — Microsoft Azure AD​

Date: 2026-02-01 | Status: Accepted

Decision: Microsoft Azure AD (Entra ID) is the source of truth for employee identity. Core fields (name, email, job title, department, manager) are synced from Graph API. BonardaHR enriches profiles with HR-specific data (configurable sections, time-off, etc.).

Dev Mode: A DevDataSeeder (@Profile("dev")) seeds test employees on startup without requiring Azure credentials.


ADR-010: Document Storage — SharePoint Integration​

Date: 2026-02-01 | Status: Accepted

Decision: Documents are stored in and served from SharePoint. The system references SharePoint document IDs rather than storing files locally. All document access flows through Microsoft Graph API using the user's delegated token, so SharePoint enforces its own access control.

Consequences: No local document storage needed; SharePoint permissions respected automatically; offline access not supported.


ADR-011: Authentication & RBAC with JWT and Dev Mode​

Date: 2026-02-01 | Status: Accepted

Decision:

  • Spring Security with stateless JWT sessions
  • @PreAuthorize method-level security using permission strings (e.g., EMPLOYEE_CREATE)
  • Four default roles seeded by Flyway: ADMIN, HR_MANAGER, MANAGER, EMPLOYEE
  • Dev profile provides POST /api/v1/auth/dev-login (absent from production builds)
  • DevAuthController and DevDataSeeder are @Profile("dev") only

ADR-012: Dual-Identifier Pattern (UUID Public IDs)​

Date: 2026-02-01 | Status: Accepted

Context: Auto-incrementing IDs (/employees/5) leak record counts and are guessable (IDOR risk).

Decision: Every entity has:

  • BIGSERIAL id — for foreign keys, joins, JPA; never exposed via API
  • UUID public_id — the only identifier returned to clients

Mapping Boundary:

  • Controllers accept/return UUIDs (as String in DTOs)
  • Services resolve UUIDs → entities via findByPublicId(), then use internal IDs
  • Repositories use Long id for all joins

Alternatives Considered: UUID as PK (larger indexes, B-tree fragmentation), Hashids (collision risk), exposing BIGSERIAL (IDOR).


ADR-013: Time Off Management — Workflow and Balance Tracking​

Date: 2026-02-01 | Status: Accepted

Decision: Denormalised balance counters (pending, used, allocated) updated in real-time across create/review/cancel operations. Pessimistic locking on balance rows prevents double-approval races.

State Lifecycle: PENDING → APPROVED / REJECTED / CANCELLED

Consequences: O(1) balance reads; balance consistency enforced via DB-level CHECK constraints.


ADR-014: Section-Level Visibility Permissions​

Date: 2026-02-01 | Status: Accepted

Decision: Each employee_section has an optional required_permission column. If set, a user must have that permission to view the section on another employee's profile. NULL = visible to all.

Example: The payroll section requires SECTION_PAYROLL_VIEW, which is only assigned to HR_MANAGER and ADMIN roles.


ADR-015a: Error Handling Strategy​

Date: 2026-02-01 | Status: Accepted

Decision: GlobalExceptionHandler (@RestControllerAdvice) maps all domain exceptions to consistent HTTP responses without leaking internal details. Frontend uses React ErrorBoundary for render errors.

ExceptionHTTP Status
ResourceNotFoundException404
ForbiddenException403
ValidationException / BadRequestException400
DataExistsException409

ADR-015b: Mock SharePoint Service for Local Development​

Date: 2026-02-02 | Status: Accepted

Decision: A MockSharePointService implements the SharePointService interface, activated when microsoft.graph.mock-enabled=true (the default). It provides realistic fake data (2 SharePoint sites, document drives with folders and files) without requiring Azure credentials.

Configuration:

# Default (mock — no credentials needed):
microsoft.graph.mock-enabled: true

# Production (real Graph API):
microsoft.graph.mock-enabled: false

ADR-016: Testing Microsoft Integrations (SSO + SharePoint)​

Date: 2026-02-03 | Status: Accepted

Dev Workflow Summary:

ScenarioConfigWhat Works
Quick local devDefault (no env vars)Mock SharePoint data + dev login
SSO testing onlySet AZURE_AD_* env vars (free Azure account)Real Microsoft login + mock SharePoint
Full integrationSet AZURE_AD_* + MS_* + SP_MOCK_ENABLED=falseReal SSO + real SharePoint

Option A (Recommended): Free Azure account — includes Azure AD at no cost, sufficient for SSO testing.
Option B: M365 Developer Program (if eligible) — full E5 sandbox with SharePoint and 25 test users.
Option C: No Azure account — default dev mode with mocks.


ADR-017: Backend Entity Resolution Pattern​

Date: 2026-02-06 | Status: Accepted

Context: UUID → entity resolution was duplicated across all services.

Decision: Centralised EntityResolutionService providing type-safe entity resolution with standardised error messages.

public Employee resolveEmployee(UUID publicId) {
return employeeRepository.findByPublicId(publicId)
.orElseThrow(() -> new ResourceNotFoundException("Employee", publicId));
}

Consequences: Single point of change for error message format; easy to add logging or caching.


ADR-018: Backend Enum Parsing Utility​

Date: 2026-02-06 | Status: Accepted

Decision: Centralised EnumParser utility for consistent, case-insensitive enum parsing with user-friendly error messages that list valid values.

EventType eventType = EnumParser.parse(EventType.class, request.getEventType(), "eventType");

ADR-019: Backend Application Constants​

Date: 2026-02-06 | Status: Accepted

Decision: Centralised AppConstants class for application-wide magic values (pagination sizes, validation limits, date formats, business rule thresholds). Provides compile-time safety and IDE autocomplete over scattered literals.


ADR-020: Frontend Modal Component Composition​

Date: 2026-02-06 | Status: Accepted

Decision: Modals are built from composable primitive components (Modal, ModalHeader, ModalBody, ModalFooter) rather than a single monolithic modal. This allows arbitrary content layouts without fighting a rigid prop API.


ADR-021: Frontend Form Field Components​

Date: 2026-02-06 | Status: Accepted

Decision: Standardised form field components (FormField, TextInput, SelectInput, DateInput, etc.) that wrap native inputs with consistent label, error, and helper text rendering. Integrated with react-hook-form via Controller.


ADR-022: Frontend Centralized Constants​

Date: 2026-02-06 | Status: Accepted

Decision: constants/ directory contains typed constant maps (status labels, colours, permission keys, route paths) shared across all frontend modules, avoiding magic strings and enabling refactoring.


ADR-023: Calendar Sync — Best-Effort Pattern​

Date: 2026-02-10 | Status: Accepted

Context: Calendar sync to Outlook can fail (Graph API error, no microsoftUserId, missing Calendars.ReadWrite permission) without this being a reason to roll back a time-off approval.

Decision: Calendar sync is best-effort. If sync fails, the time-off approval proceeds normally and a calendarSynced: false flag is recorded. The sync failure is logged but does not surface as an error to the approving manager.

Consequences: Time-off approvals are always reliable regardless of Microsoft Graph availability. Sync failures are observable via logs and the calendarSynced field on approved requests.


ADR-024: Company-Wide Documents​

Date: 2026-02-12 | Status: Accepted

Decision: Company-wide policy documents (employee handbooks, HR policies) are stored in dedicated SharePoint libraries (Policies, Personnel). BonardaHR surfaces these as "Quick Access" libraries with pre-configured site/drive IDs, separate from employee-specific documents.


ADR-025: Email Notification System​

Date: 2026-02-14 | Status: Accepted

Decision: Email notifications are sent via Spring Mail (JavaMailSender) with HTML Thymeleaf templates. Notifications are triggered by domain events (time-off created, reviewed; timesheet overdue). Failed sends are logged but do not fail the triggering operation (same best-effort pattern as calendar sync).

Key templates: time-off request notification (to manager), time-off decision notification (to employee), timesheet reminder (to employee).


ADR-026: Reports & Analytics with Bradford Factor​

Date: 2026-02-18 | Status: Accepted

Context: HR needed analytics on absenteeism patterns. The Bradford Factor (S² Ɨ D) is an industry-standard formula for measuring the impact of short, frequent absences.

Decision: Implement Bradford Factor in ReportsServiceImpl using a rolling 52-week window. Only leave types with counts_towards_bradford = true (e.g. sick leave) are counted. Risk thresholds (LOW / MEDIUM / HIGH / CRITICAL) are stored in a bradford_settings single-row table and are admin-configurable.

Visibility:

  • HR Managers: all employees
  • Managers: their direct team
  • Employees: their own score only

Formula: B = S² Ɨ D

  • S = number of separate absence spells
  • D = total days absent (52-week rolling window)

ADR-027: Unified Event Bus for Task List & Workflow Execution​

Date: 2026-02-25 | Status: Accepted

Context: The workflow engine was initially template-only. Task lists (ad-hoc multi-step checklists) needed to share the same execution infrastructure without duplicating the engine.

Decision: Introduce a WorkflowEventBus that dispatches WorkflowEvent objects to matching task lists and workflow templates. Domain services (EmployeeServiceImpl, TimeOffRequestServiceImpl) publish events via eventBus.publish(). A DateFieldScheduler (cron: 6:05 AM daily) publishes date-based events (hire anniversaries, leave start/end dates).

Extensibility: Adding a new event source requires one @Component implementing WorkflowEventSource. The catalog auto-discovers it; the frontend dropdowns auto-populate. No other files need to change.

Alternatives Considered: Direct coupling (O(N) files per new source), Spring ApplicationEvent (synchronous by default, no catalog), admin-configured custom events (disconnected from real domain actions).

Known Performance Considerations (acceptable at 200 employees):

ConcernMitigation When Needed
N+1 queries in SectionDateFieldSourceBatch pre-load all section field values
Repeated task list queries per eventCache task list results per scheduler run
@Async without backpressureConfigure bounded executor with rejection policy
Deduplication race windowAdd workflow_event_log table with unique constraint

Future ADRs​

As the project evolves, the following topics will be documented: