Designing Governance for Enterprise Applications
Your audit logs can tell you which database columns changed. Can they tell you who authorized the change, under whose authority it happened, and which business operation caused it? This post lays out a governance architecture that captures accountability and delegation as first-class concerns, so your system can explain itself when an auditor comes knocking.
ENTERPRISE ARCHITECTURE
Prasad Bhamidipati
5/5/20266 min read
Most enterprise frameworks ship with solid authentication, authorization, and persistence layers. Governance gets treated differently. It ends up scattered across ad-hoc audit tables, inconsistent logging calls, and one-off compliance patches bolted on before an audit deadline. The result is a system that can tell you what data changed but struggles to explain why it changed, who authorized the change, and what business operation caused it.
If you're building applications in healthcare, financial services, identity management, or enterprise SaaS, governance is a design problem worth solving early. Retrofitting it is painful, and the ad-hoc version rarely survives its first real audit.
This post lays out an approach to governance architecture that I've found effective. It covers the core abstractions, the data model decisions that matter, and the places where most teams get tripped up.
Treat governance as its own concern
Authorization decides whether an action should be allowed. Security protects systems from unauthorized access. Governance does neither of those things. Governance records what actually happened.
That distinction matters because the questions governance must answer are different in kind from the questions authorization answers. An auditor asking "who approved this loan and under whose authority?" is asking a governance question. The authorization layer already did its job at request time; it has no opinion about what happened six months later during an investigation.
When governance is tangled up with authorization or buried inside entity auditing, teams end up with audit logs that can answer "which database columns changed?" but cannot connect those changes to a business operation, an identity, or a chain of authority. Governance deserves its own data model, its own recording infrastructure, and its own place in the architecture.
Record business operations, not just field changes
Traditional entity auditing records field-level changes. You get rows like `loan.status: PENDING -> APPROVED` and `credit_limit: 10000 -> 15000`. Useful, but incomplete. Was that change triggered by an underwriter approving the loan through the UI? A support engineer overriding a decision? A nightly batch job catching up on pending applications?
A well-designed governance layer introduces an explicit concept for business operations. Call it a Governance Action, an Operation Record, whatever fits your domain. The point is to capture the business intent: `LoanApproved`, `PatientRecordViewed`, `DatasetExported`, `ConfigurationUpdated`. These are domain-level events, not database triggers.
Each governance action links to zero or many entity-level audit records. A `PatientRecordViewed` action produces no data mutations at all, but it still needs to exist in the governance trail. A `LoanApproved` action might produce three or four audit records across different entities. The governance action is what ties those mutations to a single operation and the identities behind it.
This two-layer design (business operations + field-level changes) gives investigators the ability to work in both directions: start from a business operation and see what data changed, or start from a suspicious data change and trace it back to the operation and identities responsible.
Without that connective layer, you're left with a pile of field diffs and no story.
Separate the actor from the authority
Every governance record should capture two identities: the actor (who physically executed the operation) and the principal (whose authority or privileges enabled it).
In most cases, these are the same person. Alice approves a loan; actor is Alice, principal is Alice. But enterprise systems are full of scenarios where they diverge:
An assistant approves a loan on behalf of a manager. Actor: assistant. Principal: manager.
A support engineer modifies a customer's configuration. Actor: support engineer. Principal: customer admin.
A nurse accesses a patient record under a physician's authority. Actor: nurse. Principal: physician.
A scheduled job runs a billing process created by a finance director. Actor: the scheduler service. Principal: the finance director.
If your governance records only store a single "user" field, you lose the ability to answer "who actually did this?" separately from "whose authority made this possible?" That's a problem during any serious investigation, and it makes delegation invisible to auditors.
A useful invariant to enforce: The actor identity is never overwritten by the principal. Both are always preserved. Detecting delegation then becomes a trivial check: if the actor and principal differ, delegated authority was involved. That one comparison unlocks a whole class of governance queries without requiring a separate delegation tracking system.
Never delete an identity
Governance records must remain meaningful for years, sometimes decades. If an employee leaves and their identity gets deleted from the system, every governance record referencing that identity loses its meaning. `actor_identity = NULL` tells an auditor nothing.
Design your identity model so that identities referenced in governance records are never deleted. Instead, move them through lifecycle states: `ACTIVE`, `SUSPENDED`, `DISABLED`, `ARCHIVED`. When Alice leaves the company, her identity becomes `DISABLED`. The governance records that reference her remain intact and interpretable.
This creates a practical problem worth thinking about early: long-running automation. If Alice created a recurring billing job and then left, the job's principal identity now points to a disabled identity. You have a few options. You can suspend the job automatically, which is safest for security-sensitive operations. You can let it keep running with the historical principal, which works for low-risk analytics jobs. Or you can flag it for administrative reassignment, which is the most operationally sound approach for most cases. Whichever you pick, make sure the governance trail records the decision.
Consider also storing snapshot fields like `actor_display_name` at the time of the action. Even if Alice's canonical identity record is later updated after a name change, the governance record preserves what the system knew when the action occurred.
Factor out execution context
During a user session, many governance-relevant attributes stay constant: same identity, same device, same IP address, same authentication method. Repeating all of this on every governance record bloats the data model and creates consistency risks.
A cleaner approach is to introduce an execution context that captures this environmental metadata once and let governance actions reference it. One execution context can parent many governance actions.
Make execution contexts more general than sessions. A session implies a human user in a browser, but governance actions also come from API requests, scheduled jobs, background workers, and integration connectors. A `context_type` field that distinguishes `USER_SESSION` from `SCHEDULED_JOB` from `API_REQUEST` lets you unify all of these under a single abstraction without pretending they're the same thing.
The hardest part of execution context is propagation. The context must follow the request through every layer of the system, including asynchronous task execution. If a user request enqueues a background job, that job's governance actions need to reference the original execution context (or a child context that links back to it). Without propagation, governance actions recorded inside async workers lose their connection to the identity and environment that initiated the operation. This is a framework-level concern; leaving it to individual developers guarantees inconsistency.
Apply the same model to automation
A common gap in governance designs is how they treat automated processes. Many systems record human-driven actions carefully but let scheduled jobs and background workers operate without attribution.
Apply the actor-principal model to automation the same way you apply it to human users. When a scheduler triggers a job, the platform service that executes the job is the actor. The identity that created or authorized the job is the principal. Jobs created by integration connectors follow the same pattern: the connector identity becomes the principal, the scheduler remains the actor. System-owned jobs (database cleanup, index rebuilding) use a reserved system identity as the principal, which clearly distinguishes internal platform operations from user-authorized automation.
Recording a `trigger_source` on governance actions (`USER_ACTION`, `SCHEDULED_JOB`, `API_CALL`, `INTEGRATION_EVENT`) makes it easy to filter and investigate automation-driven changes without special-case logic.
Design the data model as an append-only hierarchy
The governance data model works well as a clean hierarchy: Account -> Identity -> Execution Context -> Governance Action -> Audit Record.
An Account provides multi-tenant isolation. Identities belong to accounts and come in types that distinguish human users from service accounts from system-level actors. Each Execution Context references an actor identity, a principal identity, and environmental metadata. Each Governance Action references an execution context and records the business operation, the affected resource, and the trigger source. Audit Records hang off governance actions and capture entity-level field changes.
Two extensions are worth considering depending on your domain. A Delegation table can record explicit delegation relationships with scope and time bounds (a manager grants loan approval authority to an assistant until year-end). A Lineage table can capture derivation relationships between entities (this invoice was generated from that order, which was derived from that quote), which is valuable in financial systems where data provenance matters.
All governance data should be append-only. Actions and audit records are immutable once written. Identities are never deleted. Referential integrity between governance actions, identities, and accounts is maintained through foreign keys. This produces a governance store that survives years of personnel changes, organizational restructuring, and system evolution while remaining queryable and defensible.
Keep governance out of business logic
A governance layer should record what happened. It should not enforce compliance policies, evaluate authorization rules, or manage approval workflows. HIPAA, SOX, PCI, and whatever regulatory alphabet your industry requires remain the application's responsibility. Governance gives those applications a reliable substrate for recording their operations so that when an investigator arrives, the system can explain itself.
The practical test: can a developer add a new governed operation without writing audit-specific code in their service method? If the answer is yes, if the framework handles context propagation, identity capture, and action recording automatically, then governance becomes a property of the system rather than a burden on every feature team. If the answer is no, you'll get inconsistent coverage, gaps in the audit trail, and governance logic mixed into domain code where it doesn't belong.
Getting governance infrastructure right at the framework level, so that application teams inherit it instead of reinventing it, is the design decision that determines whether your system is explainable by default or only explainable where someone remembered to add logging.
Expert articles on AI and enterprise architecture.
Connect
prasadbhamidi@gmail.com
+91- 9686800599
© 2024. All rights reserved.