At work

eclaim

Expense claim and reimbursement workflow. Two-tier amount-based approval, finance review, immutable audit trail.

StatusIn development
v1 in development: domain design and API contract complete, authentication in progress
StackJava 21 · Spring Boot · Spring Security · JPA · PostgreSQL · JWT · OpenAPI · React
Updated27 Aug 2026
LinksPrivate repo

Screenshots

Screenshots land with the first demoable slice.

What it does

  • A claimant groups related expenses — hotel, taxi, client dinner — into one claim with per-item receipts. The backend derives the total; the client never sends it.
  • Submission routes the claim to the claimant’s manager. Above a configurable threshold (RM 2,000 by default) it also needs the manager’s manager. A top-level executive with no manager uses a designated approver instead.
  • An approver can approve, return (recoverable — the claimant corrects and resubmits) or reject (terminal). Return and reject both require a comment.
  • Management-approved claims land in a shared Finance queue for compliance review. Finance records the reimbursement once payment has completed in the external banking system.
  • Every significant command and transition appends an immutable audit event in the same transaction. Claims outside a user’s visibility return 404, not 403.

Why I built it

This is the expense-claim system I work on at [telco — TODO(weilok): confirm wording], rebuilt from scratch in Spring Boot. Same business rules — amount-threshold routing to a second-level approver, shared Finance queue, return-vs-reject, immutable approval history — designed and implemented end to end so I own every decision from the data model to the API contract.

It’s a two-person build. I own the backend; a collaborator owns the React front end, built against a contract-first OpenAPI spec with mocked responses so neither track waits on the other. Every feature is a pull request into dev, cross-reviewed before merge.

Under the hood

Spring Boot modular monolith · PostgreSQL · stateless JWT · React SPA behind an OpenAPI contract.

Claim lifecycle:

StateMeaningEditable by
DRAFTBeing preparedOwner
PENDING_MANAGERWaiting on the snapshotted first approver
PENDING_SECOND_LEVELWaiting on the snapshotted second approver
PENDING_FINANCEManagement-approved; in the shared Finance queue
RETURNEDCorrectable, outside the active workflowOwner
REJECTEDTerminal denial
REIMBURSEDExternal payment recorded; terminal success

Roles are deliberately thin. FINANCE and ADMIN are the only system roles. Claiming is a baseline capability, and approval authority comes from a claim-specific assignment rather than a global “manager” role. Admin manages reference data and reads the audit log but can never approve or reimburse.

Engineering notes

These decisions are locked in the spec; the implementation is landing milestone by milestone — see Status below.

Resubmission starts a new approval round

When a returned claim is edited and resubmitted, the earlier approvals aren’t overwritten. A new approval round is created and approval restarts from the first approver, because the earlier approvers reviewed the earlier amount and receipts — they never approved the corrected version. It also means a RM 1,900 claim returned and corrected to RM 2,500 is re-routed through second-level approval: routing uses current data at round creation.

Cost: queries and UI group steps by round. Revisit when: the business identifies edits that don’t invalidate prior approvals.

Approvers are snapshotted on the step

Steps are created lazily and record the assigned approver at creation time. If Admin reorganises the org chart mid-flight, in-progress steps keep their approver; only new steps and rounds resolve against the current organisation. Deactivating a user is blocked while they own pending steps.

A receipt belongs to a line item

Receipts aren’t claim-level attachments. Each one supports exactly one line item, and the item’s category decides whether a receipt is mandatory. One hotel receipt can’t accidentally validate the taxi and the dinner.

Hand-rolled state machine and audit log

Spring StateMachine and Hibernate Envers were both available and both rejected for v1. Writing the transition guards and the audit append by hand is the point: I wanted to know exactly what a guarded transition costs and what an audit row has to capture before adopting a library that hides it. Every guard checks state, actor and assignment together.

First valid commit wins

Two Finance users acting on the same claim is the expected race, not an edge case. Transitions use optimistic locking; the second, stale action gets a 409 rather than silently overwriting the first.

Status & roadmap

Done: domain design (use cases, 20 numbered business rules, 12 acceptance scenarios, decision log) · target PostgreSQL schema · slice-0 OpenAPI contract · Postgres dev environment · user, role and claim entities.

In progress: JWT authentication (login, /api/me).

Next: claim drafting API → submit and routing → approval decisions → Finance review and reimbursement → audit timeline → CI/CD deployment.

Last updated 27 Aug 2026 · ← All work