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:
- User authenticates via Microsoft Azure AD
- Backend receives OAuth2 token, validates with Microsoft
- Backend creates/links an Employee record
- Backend generates JWT with user claims and permissions
- Frontend stores JWT and uses it for all API requests
- 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+sizequery 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
@PreAuthorizemethod-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) DevAuthControllerandDevDataSeederare@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 APIUUID public_idā the only identifier returned to clients
Mapping Boundary:
- Controllers accept/return UUIDs (as
Stringin DTOs) - Services resolve UUIDs ā entities via
findByPublicId(), then use internal IDs - Repositories use
Long idfor 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.
| Exception | HTTP Status |
|---|---|
ResourceNotFoundException | 404 |
ForbiddenException | 403 |
ValidationException / BadRequestException | 400 |
DataExistsException | 409 |
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:
| Scenario | Config | What Works |
|---|---|---|
| Quick local dev | Default (no env vars) | Mock SharePoint data + dev login |
| SSO testing only | Set AZURE_AD_* env vars (free Azure account) | Real Microsoft login + mock SharePoint |
| Full integration | Set AZURE_AD_* + MS_* + SP_MOCK_ENABLED=false | Real 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):
| Concern | Mitigation When Needed |
|---|---|
N+1 queries in SectionDateFieldSource | Batch pre-load all section field values |
| Repeated task list queries per event | Cache task list results per scheduler run |
@Async without backpressure | Configure bounded executor with rejection policy |
| Deduplication race window | Add workflow_event_log table with unique constraint |
Future ADRsā
As the project evolves, the following topics will be documented: