Skip to content
TankDev

How Do You Design a Modern Enterprise Software Architecture?

Design a production system with APIs, PostgreSQL, permissions, jobs, backups, and scaling. Explore practical C++/Python and library trade-offs.

· TankDev Mühendislik

A modern enterprise architecture connects interfaces, business rules, data, and operations through clear responsibilities. Good design supports more than successful transactions: it defines what happens when two people edit the same record, an ERP does not respond, or a new release introduces a problem.

Next.js, FastAPI, and PostgreSQL can support this architecture. Comparable functionality can also be built with other frameworks, in Python or C++. The meaningful choice concerns the performance, delivery time, maintenance, and operating requirements met alongside the functionality.

We will design an illustrative system in which sales prepares a quotation, a manager approves it, and the approved quotation becomes an order in the ERP. The examples explain architectural decisions; they are not measured client results.

1. Define the rules to protect before designing screens

Start with what the system must prevent. In this scenario, a user must not see another company's quotation, an approved amount must not change silently, and a repeated transfer request must not create a second ERP order.

Add operational targets: expected concurrency, peak transaction volume, response-time goals, acceptable data loss, and tolerable downtime. Every module need not have identical availability targets. Approving an order and reviewing last year's report may have different priorities.

A modular monolith with customer, quotation, approval, and integration modules is a reasonable starting point: one application with clear internal boundaries. It can run a separate worker process without turning every module into a microservice. Consider further separation when independent delivery or distinct scaling requirements justify it.

A browser reaches the Next.js interface and REST API over HTTPS. FastAPI business modules use PostgreSQL; an outbox and queue connect a worker to the ERP. Identity, documents, observability, and backups have defined supporting responsibilities.

Figure 1. An illustrative technology layout. Arrows show communication, not a requirement to execute everything in one request.

2. Frontend and backend separation defines responsibility

The frontend lets users inspect information, enter data, and understand progress. The backend validates prices, permissions, approval conditions, and state transitions. A total or an “approved” field sent by the browser is not authoritative.

Next.js can contain both server-side and browser-side code. The frontend is therefore not necessarily confined to the user's computer. The Next.js server and client components guide explains this distinction. In this design, the Next.js server may aggregate data for the interface, while the backend owns quotation approval rules.

Implementing the same discount rule independently in the UI and several server layers invites inconsistency. The interface can provide early feedback, but the authoritative check belongs to one business layer. Never expose database credentials to the browser; restrict API and database access through appropriate network and application permissions.

3. REST API: Establish a clear contract

A REST API exposes resources and operations through HTTP. For example, GET /quotes/417 retrieves a quotation, while POST /quotes/417/approvals handles an approval request. Request schemas, error behaviour, pagination, and compatibility policy form part of the contract.

The approval request can include the version the user reviewed:

json
{
  "expected_version": 7,
  "note": "Delivery terms have been reviewed."
}

The server checks the authenticated user's permission and whether the quotation is still at version 7. If someone changed the quantity, reject approval from the stale screen—for example, with 409 Conflict—and ask the user to review the current record. User or company identifiers supplied by the client do not establish authority.

Recording internal approval and completing ERP transfer are separate outcomes. If integration is pending, both the response and interface should say so. A successful HTTP request does not establish that the entire business process has finished.

4. PostgreSQL: Protect integrity alongside application code

Define relationships between customers, quotations, lines, approvals, and transfer records. Use numeric types that preserve the required monetary precision, explicit quantity units, and a consistent timestamp convention. Unique keys and foreign keys help preserve relationships without relying solely on developer discipline.

Approval must check the version and update the record atomically. Reading the version and performing an unconditional write later does not resolve the race. Use appropriate locking or an update conditional on version = 7, checking the affected row count. Approval, the state change, an audit event, and an outgoing outbox record can be committed in one transaction.

That transaction does not include the remote ERP. An ORM also does not remove the need to design indexes, inspect query plans, and manage transactions. If a quotation list fetches each customer's details separately, a hundred-row screen can produce a hundred additional queries. Inspect query count, indexes, and returned data volume before considering a language change.

5. Authentication, authorisation, and RBAC answer different questions

Authentication verifies identity, potentially using a corporate identity provider and multifactor authentication. Authorisation determines what that identity may do to a particular record. Signing in does not grant access to every quotation.

Role-based access control (RBAC) groups permissions into roles such as sales, manager, and finance. Roles may need additional conditions: a manager might approve only their own company's quotations, with a second approval above a threshold. Evaluate role, record scope, and business policy together. Enforce checks server-side on every request, as described in the OWASP authorisation guidance.

Permissions extend beyond screen buttons. APIs, file downloads, reports, and background jobs must preserve the same scope. Define session and access lifecycles when employees leave or change roles. Cookie-based sessions need secure cookie settings and CSRF protection; token-based designs need signature, expiry, issuer, and audience validation. CORS is not an authorisation mechanism.

6. Audit logs, integrations, and background jobs

An audit log answers who approved a quotation, which version they approved, and when. Critical edits can preserve previous and new values or a change summary. Define access and retention, and prevent ordinary users from modifying history. Audit records serve a different purpose from technical debugging logs.

ERP transfers, large reports, and document conversion can run as background jobs. Users see a job identifier and status instead of holding a web request open. Work that must survive failure should not exist only in a web process's memory. It needs durable queuing, attempt counts, timeouts, and recovery behaviour.

An outbox publisher sends work to the queue; a worker calls the ERP. Design writes for idempotency because messages can be delivered again. If the ERP saves an order but fails to respond, use safe retry with the same operation key or check the remote status before creating another order. Our article on LLMs, APIs, and autonomous workflows explores this pattern; the same reliability principles apply without AI.

7. If C++ and Python can build the same system, what changes?

Two applications can provide the same quotation screen and API contract while consuming different resources and requiring different delivery and maintenance effort. Language choice affects those characteristics together with libraries and team experience.

Python can be convenient for frequently changing business rules and existing integrations. C++ can be advantageous for intensive computation, close interaction with native systems, and detailed control of memory layout. Neither statement guarantees a particular performance result: algorithms, data access, build options, and workload matter. C++ introduces decisions about memory lifetime and native builds; Python requires attention to its runtime, dependencies, and concurrency model.

Consider a purely hypothetical request spending 450 ms in the database, 500 ms waiting for the ERP, and 50 ms in application computation. Making computation ten times faster reduces the total from 1,000 ms to 955 ms: only a 4.5% reduction. Improving the query or moving ERP work to a background job may be more useful than rewriting the backend. Background execution does not make the ERP faster; it separates user waiting time from business completion time.

Conversely, if a production-planning algorithm spends most of its time on the CPU, algorithm improvements, an optimised native library, or a C++ component may help. The entire system need not use one language. A Python API can use a C/C++ extension or a separate computation worker; the Python extension documentation explains native interoperability. That introduces data-transfer, build, and debugging costs. Profile first, then choose the boundary.

Technology selection starts with workload measurement. Waiting-heavy problems call for examining queries, connections, and integrations; CPU-heavy problems call for examining algorithms and native computation. Compare candidates using the same acceptance criteria and total maintenance cost.

Figure 2. Real workload and operating cost distinguish implementations that offer the same functionality.

8. Why choose between Python libraries that do the same job?

Many libraries can send an HTTP request. But directly making a blocking network call inside an async API can prevent other tasks on that event loop from progressing. An async-compatible client, timeouts, and connection pooling matter in this context. The HTTPX async guide covers client lifetime and connection reuse. Async does not itself accelerate CPU computation; the FastAPI concurrency guide distinguishes waiting from parallel computation.

For data access, SQLAlchemy ORM can simplify working with objects and relationships, while writing SQL through a driver provides more explicit query control. An ORM can also coexist with direct SQL. Both approaches still need pooling, parameterised queries, transaction boundaries, and migrations. The SQLAlchemy Session guide explains session and transaction lifecycles.

Framework choice also balances supplied components against components the team must assemble. Django's ORM, administration interface, and authentication infrastructure can reduce initial work for an internal operations application. FastAPI can suit a service built around typed API contracts; administration and user lifecycle requirements still need an implementation choice. Both can support enterprise applications with appropriate design.

Try the same small scenario in each candidate: create a quotation, roll back a transaction, reject unauthorised access, handle a timeout, and emit a metric. Evaluate documentation, maintenance activity, licensing, security updates, and the team's debugging skills. Record the decision and the conditions that would justify revisiting it in a short architecture decision record.

9. Backups and observability belong in the first release

Recovery point objective (RPO) defines the acceptable data-loss window; recovery time objective (RTO) defines the target time to restore service. A 15-minute RPO and two-hour RTO are design targets, not automatic guarantees. A nightly backup alone cannot meet a 15-minute data-loss target throughout the day.

PostgreSQL supports point-in-time recovery (PITR) using an appropriate base backup and a continuous sequence of archived WAL. The PostgreSQL PITR documentation explains the requirements. Store backups in a separate failure domain and apply access and encryption controls. A replica alone is not a backup: it can replicate an accidental deletion too.

A restore rehearsal includes attached documents, necessary configuration, and secure access to required keys. Reconcile restored records with operations already transferred to the ERP so replayed jobs do not create duplicate orders. Measured recovery time is more useful than an untested estimate.

Observability means understanding internal behaviour through the signals a system produces. Metrics reveal error rates and latency, logs describe events, and traces show where a request spends time across the API, database, and ERP. The OpenTelemetry signals overview explains these categories. Correlate them using operation identifiers, exclude passwords and unnecessary personal information, and give every actionable alert an owner and a response procedure.

10. Deployment and scaling: Design for change as well as load

Deployment includes pinned dependencies, promoting a verified build across environments, securely supplying secrets, and sequencing migrations. Test critical permission checks, concurrent approval, and integration failure. New instances should pass readiness checks before receiving traffic; shutting-down processes should stop accepting work and finish or hand off in-flight jobs safely.

Plan database changes so old and new application versions can coexist during a transition: add a compatible field, backfill if needed, deploy new code, and remove the old field last. Rolling back application code does not restore deleted database content. The recovery plan must account for that distinction.

Scalability starts with measurement under realistic data and concurrency, then query, index, and pool analysis. If six API processes can each open fifteen connections, that is potentially 90 connections before workers are counted. Adding application replicas can overload the database rather than increase useful capacity.

Scale the component that needs it: API replicas whose state is not tied to process memory, separate worker capacity, an appropriate cache, or a reporting read replica. Replica lag matters for decisions requiring current data, such as approvals. Cache keys must preserve company and permission scope. A single server may simplify operations; if the downtime target cannot tolerate that single point of failure, redundancy needs explicit design.

What should an architecture decision produce?

The result should include more than a technology list: ownership of business rules, data and API contracts, a permission matrix, failure behaviour, a restore procedure, and a release plan. Performance claims need measurements; technology choices need reasons the team can sustain.

Tracing a quotation from the interface to the ERP, preventing an invalid approval, and recovering safely from a failed release are stronger architectural evidence than the name of the language. Evaluate C++, Python, or an alternative library by the cost and reliability with which it delivers those outcomes.

Related notes

WhatsAppDirect contact