$ open blueprint
TalentFlow HCM is designed as a workforce operating system rather than a collection of disconnected HR CRUD modules.
The platform manages the complete employee lifecycle:
Candidate
│
▼
Recruitment
│
▼
Offer
│
▼
Onboarding
│
▼
Active Employee
│
├──────────────► Attendance
│
├──────────────► Leave
│
├──────────────► Compensation
│
├──────────────► Payroll
│
├──────────────► Performance
│
└──────────────► Workforce Analytics
│
▼
Transfer / Promotion / Compensation Change
│
▼
Offboarding
│
▼
Employment History
The important architectural decision is that an employee is not treated as a static database record.
An employee is the current representation of a continuously changing employment relationship.
That means:
Employee
+
Employment History
+
Organizational Assignment
+
Compensation History
+
Attendance
+
Leave
+
Performance
+
Payroll
+
Lifecycle Events
must be modeled as related but independently governed domains.
Business Problem
A traditional HR system often starts as a simple employee table:
Employee
---------
name
department
salary
joining_date
status
This works until the organization starts changing.
An employee can:
- change departments
- change managers
- receive a promotion
- receive a salary adjustment
- move between branches
- change employment type
- take leave
- work different shifts
- receive bonuses
- become eligible for new benefits
- participate in performance cycles
- leave the organization
If the system simply overwrites the employee record, historical truth disappears.
For example:
Employee.salary = 150000
does not answer:
What was the salary in January?
Who approved the increase?
When did it become effective?
Which payroll period used the old salary?
Which payroll period used the new salary?
What compensation structure was active at that time?
Therefore TalentFlow treats workforce state as time-aware domain data rather than a collection of mutable fields.
Architectural Goals
The platform is designed around several non-negotiable requirements.
| Requirement | Architectural Decision |
|---|---|
| Tenant isolation | Organization-scoped data access |
| Employee history | Effective-dated records |
| Payroll correctness | Transactional calculation + immutable payroll results |
| Workflow governance | Explicit state machines |
| Authorization | RBAC + resource-level scope |
| Reporting | CQRS read models |
| Background processing | Celery workers |
| Real-time state | Redis |
| Auditability | Append-only audit events |
| Integration | API + domain events |
| Reliability | Idempotent commands and jobs |
| Scalability | Domain separation + asynchronous processing |
The system is intentionally designed so that operational correctness does not depend on dashboards, background jobs, or external integrations.
High-Level Architecture Blueprint
┌───────────────────────────────┐
│ Client Applications │
│ │
│ Web │ Mobile │ Admin │ ESS │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ API Gateway │
│ │
│ JWT │ Tenant Context │ RBAC │
│ Rate Limit │ Validation │
└───────────────┬───────────────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Employee Domain │ │ Recruitment │ │ Payroll Domain │
│ │ │ Domain │ │ │
│ Lifecycle │ │ Candidates │ │ Payroll Runs │
│ Employment │ │ Jobs │ │ Salary │
│ Org Assignment │ │ Interviews │ │ Benefits │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Attendance │ │ Leave Domain │ │ Performance │
│ Domain │ │ │ │ Domain │
│ │ │ Requests │ │ Goals │
│ Timesheets │ │ Policies │ │ Reviews │
│ Shifts │ │ Balances │ │ Cycles │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└─────────────────────────┼─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Domain Event Layer │
│ │
│ Employee Events │
│ Payroll Events │
│ Attendance Events │
│ Leave Events │
│ Performance Events │
└────────────┬────────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PostgreSQL │ │ Redis │ │ Celery │
│ │ │ │ │ Workers │
│ Source of │ │ Cache / │ │ │
│ Truth │ │ Locks │ │ Async Jobs │
└──────┬──────┘ └─────────────┘ └──────┬──────┘
│ │
▼ ▼
┌──────────────┐ ┌────────────────────┐
│ Read Models │ │ External Systems │
│ │ │ │
│ Analytics │ │ Email │
│ Dashboards │ │ Banking │
│ Reports │ │ Tax APIs │
└──────────────┘ │ Attendance Devices │
└────────────────────┘
Domain Architecture
The platform is divided into bounded domains rather than one large
EmployeeService.
TalentFlow
│
├── Identity & Access
│
├── Organization
│
├── Employee
│
├── Recruitment
│
├── Onboarding
│
├── Compensation
│
├── Attendance
│
├── Leave
│
├── Payroll
│
├── Performance
│
├── Workflow
│
├── Notification
│
├── Audit
│
└── Analytics
Each domain owns its business rules and persistence boundaries.
A domain can publish events to other domains, but should not directly modify another domain's tables.
Example
Bad:
employee.salary = 180000
employee.save()
payroll.salary = employee.salary
payroll.save()
This creates hidden coupling.
Better:
Compensation Domain
│
▼
CompensationChanged
│
├────────► Payroll
│
├────────► Analytics
│
└────────► Audit
The compensation domain owns the change.
Other domains react to the fact that the change occurred.
Tenant Architecture
TalentFlow is designed as a multi-tenant SaaS platform.
Platform
│
├── Organization A
│ ├── Branches
│ ├── Departments
│ ├── Employees
│ └── Payroll
│
├── Organization B
│ ├── Branches
│ ├── Departments
│ ├── Employees
│ └── Payroll
│
└── Organization C
├── Branches
├── Departments
├── Employees
└── Payroll
Every tenant-owned record carries organization context.
class TenantModel(models.Model):
organization = models.ForeignKey(
Organization,
on_delete=models.PROTECT,
)
class Meta:
abstract = True
Tenant context should be established before business logic executes.
JWT
│
▼
User
│
▼
Organization Membership
│
▼
Tenant Context
│
▼
Permission Evaluation
│
▼
Domain Operation
A request should never be able to choose its organization arbitrarily.
Bad:
GET /api/employees?organization_id=abc
Good:
Authenticated User
│
▼
Resolved Organization Context
│
▼
Query automatically scoped to tenant
Tenant Isolation
Every query must be tenant-safe.
Bad:
Employee.objects.filter(id=employee_id)
Better:
Employee.objects.filter(
organization=request.organization,
id=employee_id,
)
Even better is to make tenant scoping part of the repository/query layer so developers cannot accidentally omit it.
EmployeeQuery.for_tenant(
organization=request.organization
).get(id=employee_id)
The tenant boundary is therefore treated as a security invariant rather than a developer convention.
Organizational Hierarchy
Organizations usually have more than a flat employee list.
Organization
│
├── Business Unit
│ │
│ ├── Department
│ │ │
│ │ ├── Team
│ │ │
│ │ └── Employees
│ │
│ └── Department
│
└── Branch
│
└── Employees
The organizational model must support:
- departments
- teams
- branches
- managers
- reporting relationships
- cost centers
- job positions
- employment types
An employee's organizational assignment should therefore be effective dated.
Employee
│
└── EmploymentAssignment
├── department
├── manager
├── branch
├── position
├── effective_from
└── effective_to
This prevents historical organizational data from being destroyed.
Employee Lifecycle Architecture
Employee lifecycle is modeled as an explicit state machine.
Candidate
│
▼
Offer Extended
│
▼
Offer Accepted
│
▼
Onboarding
│
▼
Active
│
├──────────────► Leave of Absence
│
├──────────────► Suspended
│
├──────────────► Transferred
│
└──────────────► Promoted
│
▼
Notice Period
│
▼
Offboarding
│
▼
Terminated
The state transition itself is a business operation.
Bad:
employee.status = "terminated"
employee.save()
Good:
EmployeeLifecycleService.terminate(
employee=employee,
effective_date=date,
reason=reason,
actor=user,
)
The service validates:
- current lifecycle state
- notice requirements
- pending payroll
- outstanding leave
- company assets
- approval requirements
- termination policy
and then creates the required domain events.
Employee Identity vs Employment Relationship
One of the most important modeling decisions is separating the person from their employment relationship.
Person
│
├── Personal Information
│
└── Identity
│
▼
Employee
│
├── Employment
├── Compensation
├── Assignments
├── Attendance
└── Performance
A person may have multiple employment relationships over time.
For example:
Person
│
├── Employment #1
│ └── Employee at Company A
│
└── Employment #2
└── Employee at Company B
This distinction becomes particularly important in multi-tenant systems, rehiring scenarios, contractors, and historical reporting.
Compensation Architecture
Compensation should not be stored simply as:
employee.salary = 150000
Instead:
Employee
│
▼
Compensation Profile
│
├── Base Salary
├── Allowances
├── Bonuses
├── Benefits
├── Deductions
└── Effective Dates
Example:
CompensationVersion
────────────────────────────
employee_id
effective_from
effective_to
currency
base_salary
pay_frequency
status
approved_by
This allows payroll to determine exactly which compensation version applied to a specific payroll period.
Compensation Change Flow
Manager Requests Salary Change
│
▼
Approval Workflow
│
▼
HR Review
│
▼
Finance Approval
│
▼
Compensation Version Created
│
▼
CompensationChanged Event
│
├────────► Payroll
├────────► Employee Profile
├────────► Analytics
└────────► Audit
The new salary should not overwrite the old salary.
Instead:
Old Compensation
effective: Jan 1 → Jun 30
New Compensation
effective: Jul 1 → future
Historical payroll can therefore always reconstruct the correct inputs.
Recruitment Architecture
Recruitment is treated as a separate bounded domain.
Job Requisition
│
▼
Approval
│
▼
Job Published
│
▼
Candidate Application
│
▼
Screening
│
▼
Interview
│
▼
Evaluation
│
▼
Offer
│
▼
Offer Accepted
│
▼
Onboarding
Candidate state is explicit.
Applied
│
▼
Screening
│
├──► Rejected
│
▼
Interview
│
├──► Rejected
│
▼
Offer
│
├──► Declined
│
▼
Hired
Recruitment should publish:
CandidateHired
rather than directly creating payroll or employee records across modules.
Recruitment to Employee Transition
Candidate
│
▼
Offer Accepted
│
▼
Onboarding Workflow
│
├── Create Employee Identity
├── Assign Department
├── Assign Position
├── Create Compensation Profile
├── Configure Leave Eligibility
└── Configure Payroll Profile
│
▼
Employee Activated
This creates a controlled transition from recruitment state to workforce state.
Onboarding Workflow Engine
Onboarding is modeled as a workflow rather than a checklist field.
Onboarding
│
├── Identity Verification
├── Document Collection
├── Contract Signing
├── Payroll Setup
├── Benefits Enrollment
├── Equipment Assignment
└── Manager Confirmation
Each task has:
Task
────────────────
workflow_id
type
assignee
status
due_date
completed_at
completed_by
Workflow state:
Pending
│
▼
In Progress
│
▼
Completed
Tasks may run sequentially or in parallel.
┌── Payroll Setup
│
Onboarding ──────┼── Document Verification
│
├── Equipment Assignment
│
└── Manager Confirmation
The onboarding workflow completes only when required tasks are complete.
Attendance Architecture
Attendance is modeled as raw events plus derived attendance state.
Clock In
│
▼
Attendance Event
│
▼
Attendance Processing
│
▼
Daily Attendance Record
│
├── Worked Hours
├── Late Minutes
├── Overtime
└── Absence
Raw attendance events should not be overwritten.
AttendanceEvent
────────────────────
employee
event_type
event_time
source
device_id
external_event_id
Sources may include:
Web
Mobile
Biometric Device
Import
Admin Adjustment
Attendance Idempotency
Biometric devices and external systems frequently retry events.
Therefore every external attendance event should carry an idempotency key.
device_id + external_event_id
Database constraint:
UNIQUE(device_id, external_event_id)
Then:
AttendanceEvent.objects.get_or_create(
device_id=device_id,
external_event_id=external_event_id,
defaults=payload,
)
A repeated device request cannot create duplicate attendance.
Attendance Processing
Attendance calculation should be separated from event ingestion.
Raw Attendance Event
│
▼
Validation
│
▼
Normalization
│
▼
Attendance Processor
│
▼
Daily Attendance Projection
│
├── Worked Hours
├── Overtime
├── Late
└── Absence
This allows raw events to remain immutable while attendance calculations can be corrected or recalculated later.
Leave Management Architecture
Leave is governed by policy, balance, approval, and calendar constraints.
Employee
│
▼
Leave Request
│
▼
Policy Validation
│
├── Balance Available?
├── Date Valid?
├── Overlap?
├── Holiday?
└── Approval Required?
│
▼
Approval Workflow
│
▼
Approved
│
▼
Leave Balance Updated
A leave balance should not simply be manually edited.
Instead:
Opening Balance
+
Accruals
-
Approved Leave
+
Adjustments
=
Available Balance
This provides traceability.
Leave Ledger
LeaveLedgerEntry
────────────────────────
employee
leave_type
quantity
entry_type
reference
created_at
Examples:
Opening Balance +20
Monthly Accrual +2
Approved Leave -3
Administrative Adj. +1
Current balance becomes a projection of these entries.
This prevents unexplained balance mutations.
Payroll Architecture
Payroll is the most consistency-sensitive domain in the platform.
Payroll should not be:
salary * days_worked
It is a controlled calculation pipeline.
Payroll Period
│
▼
Eligibility Snapshot
│
▼
Compensation Snapshot
│
▼
Attendance Snapshot
│
▼
Leave Snapshot
│
▼
Benefits
│
▼
Deductions
│
▼
Tax / Statutory Rules
│
▼
Gross Pay
│
▼
Net Pay
│
▼
Payroll Review
│
▼
Payroll Approval
│
▼
Payroll Finalization
│
▼
Payment Instruction
The key architectural principle:
Finalized payroll must be immutable.
Payroll Run State Machine
Draft
│
▼
Calculating
│
▼
Calculated
│
▼
Under Review
│
├──► Recalculation
│
▼
Approved
│
▼
Finalized
│
▼
Payment Processing
│
▼
Paid
Failure state:
Calculating
│
▼
Calculation Failed
│
▼
Retry / Recalculate
A finalized payroll run should never be modified in place.
Corrections should create adjustment records or correction runs.
Payroll Snapshotting
Payroll cannot depend on today's employee state.
Suppose:
Employee salary changed on July 15.
Payroll period: July 1 → July 31.
The payroll engine needs to know exactly which compensation rules applied to the period.
Therefore payroll creates a snapshot.
Payroll Run
│
├── Employee Snapshot
├── Compensation Snapshot
├── Attendance Snapshot
├── Leave Snapshot
├── Benefit Snapshot
└── Deduction Snapshot
This makes payroll reproducible.
If an employee's profile changes tomorrow, finalized payroll remains unchanged.
Payroll Calculation Engine
Payroll calculation should be composed of deterministic steps.
Base Salary
│
▼
Proration
│
▼
Allowances
│
▼
Overtime
│
▼
Bonus
│
▼
Gross Earnings
│
▼
Pre-Tax Deductions
│
▼
Tax / Statutory Deductions
│
▼
Post-Tax Deductions
│
▼
Net Pay
Each calculation should produce explainable line items.
PayrollLineItem
────────────────────────
type
description
quantity
rate
amount
calculation_reference
Instead of:
Net salary = 143,500
the system can explain:
Base Salary 120,000
Housing Allowance 15,000
Transport 5,000
Overtime 8,500
Gross 148,500
Tax 3,000
Other Deduction 2,000
Net 143,500
This is critical for payroll auditability.
Payroll Concurrency Control
Only one payroll run should be actively finalized for a given tenant and period.
A database constraint can enforce this invariant.
organization_id
pay_period_id
status
with business rules preventing multiple active/finalized runs.
Finalization should use a transaction.
with transaction.atomic():
payroll_run = (
PayrollRun.objects
.select_for_update()
.get(id=run_id)
)
if payroll_run.status != "approved":
raise InvalidPayrollState()
payroll_run.status = "finalized"
payroll_run.finalized_at = timezone.now()
payroll_run.save()
The important point is that the state transition and its invariants are protected by the database transaction.
Payroll Payment Architecture
Payroll calculation and payroll payment should be separate concerns.
Payroll Engine
│
▼
Finalized Payroll
│
▼
Payment Instruction
│
▼
Bank / Payment Provider
│
▼
External Confirmation
│
▼
Payment Reconciliation
External payment APIs are unreliable.
They may:
- timeout
- retry
- return duplicate callbacks
- process successfully but fail to respond
- deliver webhooks out of order
Therefore payment instructions need idempotency keys.
organization_id
payroll_run_id
employee_id
payment_reference
Payment Reconciliation
Never assume:
API request succeeded
=
Money was transferred
Instead:
Payment Requested
│
▼
Submitted
│
▼
Provider Processing
│
├──► Failed
│
▼
Completed
│
▼
Reconciled
Webhook events should be persisted and processed idempotently.
Performance Management
Performance management is modeled as a recurring cycle.
Performance Cycle
│
▼
Goal Setting
│
▼
Progress Updates
│
▼
Manager Review
│
▼
Peer / 360 Feedback
│
▼
Final Evaluation
│
▼
Calibration
│
▼
Cycle Closed
A performance review should reference the specific cycle and employee assignment that existed during that period.
Goal Architecture
Goals are structured objects.
Goal
────────────────
employee
cycle
title
description
metric
target
weight
status
Performance score:
Goal Score × Goal Weight
│
▼
Weighted Performance Score
This allows analytics to distinguish:
Goal achievement
vs
Manager rating
vs
Peer feedback
instead of collapsing everything into one opaque score.
Approval Workflow Engine
Several domains require approvals:
Salary Change
Leave Request
Job Requisition
Expense
Promotion
Termination
Payroll
Building approval logic separately in every module creates duplication.
TalentFlow therefore uses a reusable workflow abstraction.
Business Object
│
▼
Workflow Definition
│
▼
Approval Steps
│
├── Manager
├── HR
├── Finance
└── Executive
│
▼
Workflow Decision
Example:
Salary Increase
│
▼
Manager Approval
│
▼
HR Approval
│
▼
Finance Approval
│
▼
Effective Compensation Change
Workflow State Machine
Pending
│
▼
In Progress
│
├──► Rejected
│
├──► Returned
│
▼
Approved
│
▼
Executed
A workflow transition should be explicit.
Bad:
request.status = "approved"
Good:
workflow.approve(
actor=user,
comment=comment,
)
The workflow engine validates:
- actor permission
- current state
- required approval level
- delegation
- organization scope
- transition validity
RBAC Architecture
Authorization operates at multiple levels.
Platform Role
│
▼
Organization Role
│
▼
Resource Scope
│
▼
Action Permission
Example:
HR Manager
│
├── View Employees
├── Edit Employee Profile
├── Approve Leave
└── View Payroll
But:
Branch HR Manager
may only access:
Employees
WHERE branch_id = assigned_branch
Therefore role checking alone is insufficient.
The authorization decision becomes:
Can User
perform Action
on Resource
within Tenant
within Scope?
Data Access Policy
A permission should be evaluated close to the data boundary.
EmployeePolicy.can_view(
user=user,
employee=employee,
)
not merely:
if user.is_hr:
allow()
because authorization depends on:
- tenant
- role
- branch
- department
- reporting relationship
- resource ownership
- action
Event-Driven Architecture
Domains communicate through events.
Example:
EmployeeActivated
│
├────────► Payroll
├────────► Leave
├────────► Attendance
├────────► Notification
└────────► Analytics
Another:
CompensationChanged
│
├────────► Payroll
├────────► Analytics
└────────► Audit
Another:
LeaveApproved
│
├────────► Attendance
├────────► Payroll
├────────► Calendar
└────────► Notification
The producer does not need to know how consumers implement their logic.
Transactional Event Publishing
A major failure mode in event-driven systems is:
Database transaction succeeds
│
▼
Application crashes
│
▼
Event never published
To avoid this, TalentFlow should use an outbox pattern.
Business Transaction
│
├────────► Domain State
│
└────────► Outbox Event
│
▼
Commit Transaction
│
▼
Outbox Publisher
│
▼
Event Broker
Example:
with transaction.atomic():
employee.activate()
OutboxEvent.objects.create(
event_type="employee.activated",
aggregate_id=employee.id,
payload=payload,
)
The event and business state commit together.
A worker then publishes the event asynchronously.
Outbox Processing
Outbox Table
│
▼
Unpublished Event
│
▼
Publisher Worker
│
▼
Message Broker
│
▼
Mark Published
If publishing fails:
Retry
│
▼
Retry
│
▼
Retry
No business event is silently lost.
Idempotent Event Consumers
At-least-once event delivery means consumers may receive the same event multiple times.
Therefore:
EmployeeActivated
EmployeeActivated
EmployeeActivated
must produce the same final result as one event.
Consumer pattern:
Event ID
│
▼
ProcessedEvent
│
├── Exists → Ignore
│
└── Missing
│
▼
Process
│
▼
Record Event ID
This makes retries safe.
CQRS Architecture
Operational writes and analytical reads have different requirements.
Command Side
─────────────
Create Employee
Approve Leave
Finalize Payroll
Record Attendance
Approve Promotion
│
▼
Transactional PostgreSQL
Read Side
──────────
Headcount Dashboard
Payroll Reports
Turnover Analytics
Attendance Dashboard
Recruitment Funnel
Performance Analytics
Read models are optimized specifically for queries.
Workforce Analytics Read Model
Instead of repeatedly calculating:
COUNT(employees)
JOIN departments
JOIN branches
JOIN employment_history
JOIN attendance
...
the platform maintains projections.
WorkforceSnapshot
──────────────────────
organization
date
headcount
active_employees
new_hires
terminations
on_leave
average_tenure
payroll_total
Dashboard queries then become simple and fast.
Analytics Projection Flow
EmployeeActivated
EmployeeTerminated
EmployeeTransferred
LeaveApproved
PayrollFinalized
│
▼
Analytics Consumers
│
▼
Read Model
│
▼
Dashboard
If the projection is corrupted:
Event History
│
▼
Replay
│
▼
Rebuild Read Model
This is one of the major benefits of event-driven architecture.
Redis Architecture
Redis should not become the source of truth.
It is used for:
Redis
│
├── API caching
├── Session / temporary state
├── Distributed locks
├── Rate limiting
├── Job coordination
└── Frequently accessed read projections
Examples:
employee:{id}
organization:{id}:settings
payroll:{run_id}:status
Cached data must always be disposable.
If Redis disappears:
System remains correct.
It may become slower temporarily, but correctness does not depend on cache state.
Distributed Locking
Certain operations require protection from concurrent execution.
Examples:
Payroll Finalization
Payroll Calculation
Leave Balance Rebuild
Employee Lifecycle Transition
Bulk Attendance Processing
Redis can provide a distributed lock, but the database remains the final consistency boundary.
Request
│
▼
Acquire Lock
│
▼
Database Transaction
│
▼
Commit
│
▼
Release Lock
Locks improve coordination.
They should not replace database constraints.
Background Processing
Celery handles workloads that should not block API requests.
API Request
│
├── Immediate validation
│
├── Transaction
│
└── Queue Async Work
│
▼
Celery
│
┌──────┼───────┐
▼ ▼ ▼
Email Payroll Analytics
Typical asynchronous jobs:
- payroll calculation
- bulk attendance processing
- employee notifications
- report generation
- analytics projection
- onboarding reminders
- leave accrual
- payroll payment reconciliation
Job Reliability
Every important asynchronous job should be:
Idempotent
Retryable
Observable
Bounded
For example:
Generate Payroll
│
▼
Job ID
│
▼
Check Existing Run
│
├── Already completed → return
│
▼
Process
│
▼
Persist result
A retry must not create a second payroll run.
Audit Architecture
HR systems require strong auditability.
Important actions should produce immutable audit records.
AuditEvent
────────────────────────
organization
actor
action
resource_type
resource_id
before
after
timestamp
ip_address
request_id
Examples:
Salary Changed
Employee Terminated
Payroll Approved
Leave Approved
Role Changed
Employee Record Updated
Audit records should be append-only.
Bad:
audit.status = "corrected"
Better:
Original Audit Event
│
▼
Correction Event
The historical record remains intact.
Audit vs Application Logs
These should not be confused.
Application logs:
API request failed
Database timeout
Worker crashed
Audit events:
HR Manager changed employee salary
Finance approved payroll
Admin changed employee role
Application logs help engineers debug the system.
Audit events help organizations prove what happened.
Employee Self-Service Architecture
Employee self-service should operate through the same domain APIs as administrative interfaces.
Employee Portal
│
▼
API
│
▼
Authorization
│
▼
Employee Scope
Employees can:
View Profile
Request Leave
View Payslips
Update Allowed Information
Submit Attendance Correction
View Goals
Complete Reviews
But they cannot directly modify governed records.
For example:
Employee changes bank account
│
▼
Verification / Approval Workflow
│
▼
Account Updated
not:
employee.bank_account = request.data["bank_account"]
API Architecture
The API layer follows:
HTTP
│
▼
Serializer / Request Validation
│
▼
Application Command
│
▼
Domain Service
│
▼
Repository
│
▼
PostgreSQL
The API should not contain domain logic.
Bad:
class ApproveLeaveView(APIView):
def post(self, request):
if request.user.is_manager:
leave.status = "approved"
leave.save()
Better:
class ApproveLeaveView(APIView):
def post(self, request, leave_id):
command = ApproveLeaveCommand(
leave_id=leave_id,
actor=request.user,
)
LeaveApplicationService.execute(command)
This keeps business rules independent from HTTP.
Clean Architecture
The internal structure can follow:
Interface Layer
│
▼
Application Layer
│
▼
Domain Layer
│
▼
Infrastructure Layer
Example:
apps/payroll/
├── api/
│ ├── serializers.py
│ ├── views.py
│ └── urls.py
│
├── application/
│ ├── commands.py
│ ├── handlers.py
│ └── services.py
│
├── domain/
│ ├── entities.py
│ ├── value_objects.py
│ ├── policies.py
│ └── events.py
│
├── infrastructure/
│ ├── repositories.py
│ ├── models.py
│ └── providers.py
│
└── tasks/
└── payroll_tasks.py
The domain should not depend on Django HTTP concepts.
Database Architecture
PostgreSQL is the transactional source of truth.
The database should enforce important invariants.
Examples:
UNIQUE organization + employee_number
UNIQUE organization + payroll_period
UNIQUE attendance external_event_id
CHECK salary >= 0
CHECK effective_from <= effective_to
Application validation is useful.
Database constraints are mandatory for critical invariants.
Effective-Dated Data
Several HCM entities require temporal modeling.
Examples:
Employment Assignment
Compensation
Job Position
Manager Relationship
Benefits
Leave Policy
A historical query should be possible:
What department did employee X belong to
on March 15?
Conceptually:
effective_from <= target_date
AND (
effective_to IS NULL
OR effective_to > target_date
)
This enables accurate historical reporting.
Temporal Consistency
Consider:
Promotion effective: July 1
Payroll period: June 1 → June 30
Payroll must use the compensation before July 1.
For:
Payroll period: July 1 → July 31
the new compensation applies.
Therefore payroll calculations should resolve effective-dated records against the payroll period rather than simply reading the employee's current state.
Data Ownership Matrix
| Domain | Owns |
|---|---|
| Identity | Users, authentication |
| Organization | Tenant structure |
| Employee | Employee identity and lifecycle |
| Recruitment | Candidates and hiring pipeline |
| Compensation | Salary and compensation versions |
| Attendance | Raw attendance events |
| Leave | Leave policies and balances |
| Payroll | Payroll runs and payroll results |
| Performance | Goals and reviews |
| Workflow | Approval state |
| Audit | Immutable audit events |
| Analytics | Derived reporting projections |
The principle is:
One domain owns the truth. Other domains consume it.
Cross-Domain Transaction Boundaries
Not every business operation should be one giant database transaction.
Example:
Approve Salary Change
The compensation change itself should be transactional.
Then:
CompensationChanged
is consumed asynchronously by:
Payroll
Analytics
Notification
Audit
This avoids distributed transactions across unrelated domains.
Strong vs Eventual Consistency
TalentFlow intentionally uses both.
Strong Consistency
Required for:
Payroll finalization
Leave balance deduction
Compensation version creation
Employee lifecycle transitions
Approval state changes
Tenant authorization
Eventual Consistency
Acceptable for:
Dashboards
Analytics
Notifications
Search indexes
Email
Reporting projections
The architecture does not try to make every part of the system strongly consistent.
It applies the strongest consistency model only where the business actually requires it.
Failure Handling
Production systems fail.
TalentFlow therefore assumes:
Workers crash
Requests retry
Webhooks duplicate
External APIs timeout
Messages arrive late
Redis becomes unavailable
Database connections fail
The architecture responds with:
Idempotency
+
Transactions
+
Retries
+
Outbox
+
Audit
+
Reconciliation
rather than assuming successful execution.
Example: Employee Termination
A termination is not:
employee.status = "terminated"
The actual workflow is:
Termination Requested
│
▼
Validate Authorization
│
▼
Validate Employment State
│
▼
Approval Workflow
│
▼
Termination Approved
│
▼
Transactional State Change
│
├── Employee Lifecycle Event
├── Employment End Date
└── Outbox Event
│
▼
Event Consumers
├── Payroll
├── Leave
├── Access Control
├── Benefits
├── Notification
└── Analytics
Each consumer reacts independently.
Example: Promotion
Manager Requests Promotion
│
▼
Approval Workflow
│
▼
Approved
│
▼
Create New Employment Assignment
│
▼
Create New Compensation Version
│
▼
Publish PromotionApproved
│
├──► Payroll
├──► Analytics
├──► Employee Portal
└──► Audit
The old assignment remains historically available.
Example: Leave Approval
Employee submits leave
│
▼
Leave Policy Validation
│
▼
Balance Validation
│
▼
Manager Approval
│
▼
HR Approval (if required)
│
▼
Transaction
├── Leave Request = Approved
└── Leave Ledger Entry = -N days
│
▼
LeaveApproved Event
│
├──► Attendance
├──► Payroll
├──► Calendar
└──► Notification
The balance deduction and approval state must happen atomically.
Example: Payroll Run
Create Payroll Period
│
▼
Lock Payroll Run
│
▼
Resolve Eligible Employees
│
▼
Create Snapshots
│
▼
Calculate Payroll
│
├── Compensation
├── Attendance
├── Leave
├── Benefits
├── Deductions
└── Tax Rules
│
▼
Generate Payroll Line Items
│
▼
Validation
│
▼
Payroll Review
│
▼
Approval
│
▼
Finalization
│
▼
Payment Instructions
│
▼
Provider
│
▼
Reconciliation
Every stage has an explicit state.
Reporting Architecture
Operational reporting should not create heavy queries against transactional tables.
Transactional Database
│
▼
Domain Events
│
▼
Projection Workers
│
▼
Analytics Read Models
│
▼
Reporting API
│
▼
Dashboard
Examples:
Headcount by Department
Payroll Cost by Branch
Turnover Rate
Leave Utilization
Recruitment Conversion
Average Time to Hire
Attendance Trends
Performance Distribution
Workforce Analytics Model
Analytics can maintain specialized projections.
EmployeeSnapshot
PayrollSnapshot
AttendanceSnapshot
RecruitmentSnapshot
LeaveSnapshot
PerformanceSnapshot
This avoids forcing one normalized operational schema to serve every analytical query.
Search Architecture
Employee search can eventually become expensive when organizations contain hundreds of thousands of employees.
Instead of forcing PostgreSQL to support every fuzzy search workload:
Employee Database
│
▼
Search Projection
│
▼
Search Index
The search index remains derived data.
PostgreSQL remains the source of truth.
Notification Architecture
Notifications are asynchronous.
Domain Event
│
▼
Notification Consumer
│
├── Email
├── Push
├── In-App
└── SMS
Example:
LeaveApproved
│
▼
Notification Service
│
├── Employee notification
└── Manager notification
Notification failures should never roll back payroll or leave approval.
Security Architecture
Security exists at multiple layers.
Authentication
│
▼
Tenant Isolation
│
▼
RBAC
│
▼
Resource Authorization
│
▼
Field-Level Protection
│
▼
Audit
Sensitive workforce information includes:
Salary
Bank Information
Government IDs
Tax Information
Performance Reviews
Personal Contact Information
Access to these fields should be explicitly governed.
API Security
JWT authentication establishes identity.
Authorization establishes what that identity can do.
These are separate concerns.
JWT
│
└── Who are you?
RBAC / Policy
│
└── What are you allowed to do?
Tenant Scope
│
└── Which organization's data can you access?
Resource Policy
│
└── Which specific resource can you access?
Authentication alone must never be treated as authorization.
Audit-Ready Payroll
Payroll requires especially strong auditability.
A payroll result should be explainable through:
Payroll Run
│
├── Employee Snapshot
├── Compensation Snapshot
├── Attendance Snapshot
├── Leave Snapshot
├── Benefit Snapshot
├── Deduction Snapshot
└── Calculation Line Items
Therefore an auditor can answer:
Why did this employee receive this amount?
without relying on today's employee profile.
Architecture Evolution
The platform can evolve incrementally.
Phase 1
Basic Employee Management
│
▼
Phase 2
Recruitment + Onboarding
│
▼
Phase 3
Attendance + Leave
│
▼
Phase 4
Compensation + Payroll
│
▼
Phase 5
Workflow + Approval Engine
│
▼
Phase 6
Domain Events + Outbox
│
▼
Phase 7
CQRS Analytics
│
▼
Phase 8
External Payroll / Banking Integrations
│
▼
Phase 9
Advanced Workforce Intelligence
The important point is that domain boundaries should exist early even if the initial deployment is a modular Django monolith.
Modular Monolith First
The platform does not need microservices on day one.
A strong starting architecture is:
Django Application
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
Employee Payroll Recruitment
│ │ │
▼ ▼ ▼
Attendance Leave Performance
│ │ │
└───────────────────┼───────────────────┘
│
▼
PostgreSQL
Each module maintains clear boundaries.
As scale increases, selected domains can be extracted.
For example:
Django Modular Monolith
│
▼
Payroll becomes independent service
│
▼
Attendance ingestion becomes independent service
│
▼
Analytics becomes independent pipeline
The architecture therefore scales organizationally before it scales physically.
What I Would Build Today
If I were building TalentFlow today, I would start with:
- Django + Django REST Framework for the core API and domain application
- PostgreSQL as the transactional system of record
- Redis for caching, rate limiting, coordination, and ephemeral state
- Celery for asynchronous workflows and scheduled processing
- JWT authentication for API access
- Modular Django domains rather than premature microservices
- Explicit domain services for critical business operations
- Effective-dated employment and compensation records
- Immutable payroll snapshots and finalized payroll results
- Outbox pattern for reliable domain event publication
- Idempotent consumers for all asynchronous events
- CQRS read models for workforce analytics
- Reusable workflow engine for approvals
- Append-only audit events for governance
What I Would Avoid Initially
I would explicitly avoid:
- A giant
Employeemodel containing every HR concern - Direct cross-domain database writes
- A mutable salary field as the only compensation history
- A mutable leave balance with no ledger
- Payroll calculated directly from today's employee state
- Finalized payroll records that remain editable
- Business logic inside Django views
- Using Redis as the source of truth
- Making every operation asynchronous
- Introducing microservices before domain boundaries are proven
- Running heavy workforce analytics directly against operational tables
- Treating authentication as authorization
- Relying only on application validation for financial invariants
The goal is not to make the architecture complicated.
The goal is to make the business invariants impossible to accidentally violate.
Critical Invariants
TalentFlow should explicitly protect invariants such as:
Employee belongs to exactly one tenant context.
Payroll cannot be finalized twice.
Finalized payroll cannot be mutated.
Leave cannot consume more balance than policy allows.
Attendance events cannot be duplicated.
Compensation periods cannot overlap incorrectly.
A terminated employee cannot receive normal future payroll.
Unauthorized users cannot cross tenant boundaries.
Approval transitions must follow the workflow state machine.
Domain events must not disappear after successful transactions.
These invariants are more important than the framework being used.
Production Reliability Model
The complete reliability model looks like:
┌───────────────────────┐
│ User Command │
└───────────┬───────────┘
│
▼
Validation + RBAC
│
▼
Domain Operation
│
▼
PostgreSQL Transaction
│ │
│ └──────► Outbox Event
│
▼
Commit / Rollback
│
▼
Async Consumers
│ │ │
▼ ▼ ▼
Payroll Analytics Notification
│
▼
Retry / Idempotency
│
▼
Audit / Monitoring
This creates a system where:
Synchronous path
=
business correctness
Asynchronous path
=
propagation and scale
That distinction is fundamental.
Final Architecture
The complete TalentFlow architecture can be summarized as:
┌────────────────────────────┐
│ Web / Mobile / ESS │
└─────────────┬──────────────┘
│
▼
┌────────────────────────────┐
│ API / Auth Layer │
│ JWT + Tenant + RBAC │
└─────────────┬──────────────┘
│
┌────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Employee Domain │ │ Recruitment │ │ Payroll Domain │
│ │ │ │ │ │
│ Lifecycle │ │ Candidates │ │ Calculation │
│ Employment │ │ Jobs │ │ Snapshots │
│ Assignments │ │ Interviews │ │ Finalization │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Attendance │ │ Leave │ │ Performance │
│ │ │ │ │ │
│ Raw Events │ │ Policies │ │ Goals │
│ Shifts │ │ Balances │ │ Reviews │
│ Timesheets │ │ Approvals │ │ Cycles │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└────────────────────────────┼────────────────────────────┘
│
▼
┌────────────────────────────┐
│ Domain Event Layer │
│ │
│ Outbox + Event Consumers │
└─────────────┬──────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ PostgreSQL │ │ Redis │ │ Celery │
│ │ │ │ │ │
│ Transactions │ │ Cache │ │ Async Jobs │
│ Payroll │ │ Locks │ │ Events │
│ Employees │ │ Rate Limits │ │ Notifications │
│ Audit │ │ Ephemeral │ │ Analytics │
└───────┬───────┘ └───────────────┘ └───────┬───────┘
│ │
▼ ▼
┌───────────────┐ ┌──────────────────┐
│ CQRS Read │ │ External Systems │
│ Models │ │ │
│ │ │ Banking │
│ Workforce │ │ Tax │
│ Analytics │ │ Email │
│ Dashboards │ │ Attendance │
└───────────────┘ └──────────────────┘
The architectural philosophy is simple:
Employee state is temporal.
Payroll state is immutable after finalization.
Leave is ledger-driven.
Attendance begins with immutable events.
Approvals are state machines.
Authorization is tenant + scope aware.
Business operations are transactional.
Cross-domain communication is event-driven.
Events are published reliably through an outbox.
Async consumers are idempotent.
Analytics are derived through CQRS projections.
Audit history is append-only.
Redis accelerates the system but never owns the truth.
TalentFlow HCM therefore becomes more than an HR management application.
It becomes a transactional workforce operating system where employee identity, organizational structure, compensation, attendance, leave, performance, payroll, approvals, and analytics are connected through explicit domain boundaries and governed state transitions.
The important architectural decision is not Django, PostgreSQL, Redis, or Celery individually.
It is the decision to model workforce operations as governed business state and events rather than mutable CRUD records.
That is what allows the same architecture to remain correct when the organization grows from:
50 employees
│
▼
500 employees
│
▼
5,000 employees
│
▼
50,000+ employees
without turning payroll, employee history, approvals, or workforce reporting into a collection of reconciliation problems.


