team-expense-management-system: TEMS: The Expense App That Behaves Like a Money Pipeline

How a Spring Boot backend turns reimbursement into a controlled sequence of states, approvals, and Stripe-backed payouts.

6 to 8 min read View on GitHub More from Vyshnawee

A wide mechanical relay system moves a single expense report from a receipt slot on the left through a stamped approval gate in the middle to a payout lever and audit tray on the right. The scene explains that the app is a controlled workflow, not a simple form database.
TEMS treats reimbursement as a sequence of gates, not a pile of records.
Key Takeaways

A Corporate Expense App, or a State Machine in Disguise?

The interesting thing about TEMS is not that it stores expenses. It is that it treats each expense as a controlled transition through a company. An employee submits. A manager approves. An admin pays. An audit trail records the whole path. That is a state machine with money attached.

That framing matters because it changes the unit of design. The repo is not optimizing for faster form entry. It is optimizing for enforceable sequence, visible ownership, and fewer ways to pay twice.

The core model is a pipeline of states and gates, not a single editable expense row.

The Approval Chain Is the Product

The repo’s cleanest move is structural. When an expense is created, the backend also creates a matching approval record. That means the approval workflow is not an afterthought tacked onto the expense table. It is part of the same business event.

// Simplified flow based on ExpenseService and ApprovalService
Expense expense = expenseService.createExpense(request);
Approval approval = approvalService.createApproval(expense.getId());

// Manager acts on the approval record
approvalService.updateStatus(approvalId, APPROVED, managerId);
expenseService.updateStatus(expenseId, APPROVED);

// Admin can pay only after approval
paymentService.createCheckoutSession(expenseId);

That separation gives the codebase room to enforce responsibility. The expense record can say what was spent. The approval record can say who signed off. The payment layer can say whether the company has actually reimbursed it. Each answer lives in the right place.

Why that split matters

A lot of expense apps collapse everything into one record with a status field and a few nullable columns. That looks simpler until you need auditability, role boundaries, or payment retries. TEMS pushes those concerns apart early, which is why the workflow reads like a process rather than a pile of conditionals.

A close-up of two interlocking gears shows an approval mechanism with an empty approver slot and a payment mechanism blocked by a locking pin labeled as a paid-once guard. Small tags point to expenseId, status, paidAt, and webhook retry. The image explains why the system avoids duplicate payout.
The most production-minded detail is the guard that stops a webhook retry from paying twice.

Why Stripe Shows Up in a Reimbursement App

The payment path is where the repo starts to feel real. Instead of inventing a custom payout flow, the backend creates a Stripe Checkout session and stores the expense ID in metadata so the external payment can be tied back to the internal record.

SessionCreateParams params = SessionCreateParams.builder()
    .setMode(SessionCreateParams.Mode.PAYMENT)
    .putMetadata("expenseId", String.valueOf(expenseId))
    .build();

Session session = Session.create(params);

// Webhook reconciliation
if (expense.getPaidAt() != null) return;
expense.setPaidAt(Instant.now());

That last guard is the important bit. Webhooks retry. Networks fail. External systems repeat themselves. The paidAt check turns Stripe from a risky callback source into a safe reconciliation step. That is the difference between a demo and a system that behaves like production software.

ConcernGeneric CRUD appTEMSWhy it matters
Expense creationInsert a row with a statusCreate the expense and a matching approval recordThe workflow starts with ownership, not just storage
Approval handlingUpdate the same rowTrack approval separately from the expenseCleaner audit trail and role separation
Payment reconciliationMark as paid after a callbackUse Stripe metadata and a paidAt guardRetries cannot double-pay
Role-specific viewsOne dashboard for everyoneAdmin, manager, and employee flowsEach person sees only their responsibility
AuditabilityBest effort loggingAudit-oriented entities and repositoriesFinancial actions stay traceable

Security Is Layered Around Roles, Not Just Login

TEMS does not stop at authentication. It combines JWT filtering, BCrypt password hashing, and role-aware controllers for employees, managers, and admins. That means the system is organized around responsibilities, not around a single generic signed-in user.

That matters in a financial app. The person who submits an expense should not see the same actions as the person who approves it, and neither should expose the same tools as the person who pays it. TEMS reflects that hierarchy in the backend itself, which is where it belongs.

LayerWhat TEMS usesWhat it protects
AuthenticationJWT plus BCryptIdentity and password safety
AuthorizationRole-specific controllersWho can approve, pay, or view
Transport of claimsJWT filter before auth filterConsistent request identity
Data accessDTOs and service boundariesReduced leakage between workflows

The Enterprise Signals Are in the Boring Parts

The repo’s maturity shows up in the unglamorous choices. DTOs keep API contracts separate from JPA entities. Service interfaces keep business logic off the controllers. Repositories keep data access tidy. Those are the patterns that let a project grow without collapsing into one giant application class.

The model layer also shows care with relational hygiene. Bidirectional relationships are handled carefully to avoid serialization problems, and audit logs hint that the project expects changes to matter after the fact. That is a serious signal in a money workflow.

PatternWhat it buysWhat it suggests
DTOsStable API boundariesThe author expects the frontend to evolve
Service interfacesReplaceable business logicThe code is meant to be extended, not frozen
Audit logsTraceabilityThe domain is treated as financially sensitive
Role-specific dashboardsLess UI overlapDifferent users have different jobs
JPA relationship hygieneFewer serialization trapsThe data model was designed with care

What This Project Gets Right, and What It Still Reveals

TEMS gets the central thing right. It models reimbursement as a sequence of controlled states, not as a flat list of expenses. That single choice gives the whole system more shape, more safety, and more room to grow.

It also shows its early-stage edges. Placeholder secrets, demo-style seeding, and a few implementation shortcuts remind you that this is still a project in motion. But those rough edges do not cancel the architecture. They make the architecture easier to see.

StrengthConstraintWhat to conclude
Workflow-first designStill early stageThe domain model is stronger than the polish
Stripe reconciliationNeeds careful secret handlingThe payment path is realistic, not toy-like
Role-based backendFrontend can still matureThe system is already thinking like a product
Audit and service layeringSome demo artifacts remainThe foundation is more serious than the packaging