Hey developers! 👋
Welcome back to the journey from business requirements to production-ready systems.
A founder walks into a development team and says:
"We need an ERP."
Sounds simple.
Until you start asking questions.
Where should users live?
How should organizations work?
Who owns inventory?
Where should orders be created?
Who is allowed to approve invoices?
What happens when thousands of users start using the dashboard?
Where should business rules live?
Should everything be one application?
Should we use microservices?
And eventually:
How do we build today's system without making tomorrow's system painful to change?
That's where software architecture begins.
Not with Kubernetes.
Not with Kafka.
Not with microservices.
Not even with Django vs FastAPI.
It begins with understanding what the business actually needs to do.
Let's build the architecture from that starting point.
The First Mistake: Starting With Technology
Imagine the product team gives you an ERP requirement.
A common engineering conversation might immediately become:
Django or FastAPI?
PostgreSQL or MongoDB?
Redis?
Kafka?
RabbitMQ?
Kubernetes?
Microservices?
These are valid questions.
But they're not the first questions.
The first question is:
What responsibilities does this business actually have?
An ERP might need:
ERP
│
├── Identity
├── Organizations
├── Products
├── Inventory
├── Sales
├── Purchasing
├── Billing
├── Reporting
└── Notifications
Notice something important.
These aren't technologies.
They're business capabilities.
That distinction changes how we design the entire system.
Step 1: Discover the Business Capabilities
Start by talking about what the system does.
For example:
The business needs to:
Manage employees
Manage organizations
Track products
Track inventory
Create sales orders
Handle purchasing
Generate invoices
Process payments
Produce reports
Send notifications
Now the system has a shape.
Instead of starting with:
controllers/
models/
views/
utils/
helpers/
services/
we can start with:
identity/
organization/
inventory/
sales/
purchasing/
billing/
reporting/
Why is this useful?
Because someone joining the project six months later can understand the product by looking at its structure.
The code starts reflecting the business.
And that's a powerful architectural principle:
Organize around meaningful responsibilities before organizing around technical abstractions.
Step 2: Find the Boundaries
Identifying capabilities isn't enough.
We also need to determine where one responsibility ends.
Consider an order.
An order depends on products.
Inventory also depends on products.
Billing depends on orders.
Reporting may depend on almost everything.
If every module directly knows about every other module, we eventually get something like:
Sales ───────► Inventory
│ │
▼ ▼
Billing ◄──── Products
│ │
└──────► Reporting
▲
│
Identity
At first, this feels convenient.
Need something from another module?
Just import it.
But eventually:
Change Inventory
↓
Break Sales
↓
Break Reporting
↓
Fix Five Other Modules
The problem isn't that the system has many modules.
The problem is that the responsibilities aren't properly isolated.
A healthier model is:
Identity
│
└── Who is this user?
Organization
│
└── Which business does the user belong to?
Inventory
│
└── What resources do we have?
Sales
│
└── What are we selling?
Purchasing
│
└── What are we buying?
Billing
│
└── What do we charge?
Reporting
│
└── What happened?
Each area has a reason to exist.
That's what a boundary should accomplish.
A boundary is not just a folder. It defines ownership.
Step 3: Architecture Is About Ownership
Let's take a very normal operation:
A customer places an order.
From the outside:
POST /orders
Looks simple.
Internally, it could involve:
Authenticate User
↓
Resolve Organization
↓
Check Membership
↓
Authorize Action
↓
Validate Request
↓
Validate Products
↓
Check Inventory
↓
Calculate Price
↓
Apply Discount
↓
Create Order
↓
Update Inventory
↓
Create Financial Records
↓
Record Audit Event
↓
Trigger Notifications
A beginner might put all of this inside:
CreateOrderView
Something like:
CreateOrderView
│
├── authenticate()
├── authorize()
├── validate()
├── check_inventory()
├── calculate_price()
├── apply_discount()
├── create_order()
├── update_inventory()
├── create_invoice()
├── write_audit()
├── send_email()
└── response()
It works.
Until the application grows.
The API layer slowly becomes the place where the entire business lives.
That's when architecture starts becoming difficult to maintain.
A better separation looks like:
HTTP Request
│
▼
API Layer
│
▼
Application Service
│
▼
Domain Rules
│
▼
Persistence
│
▼
Database
Now each layer has a different responsibility.
API Layer
Understands:
HTTP
Requests
Responses
Status Codes
Serialization
Application Service
Understands:
What use case are we executing?
Domain Logic
Understands:
What rules must always remain true?
Persistence
Understands:
How do we read and write data?
Database
Protects:
Data integrity
Constraints
Transactions
Indexes
Relationships
The objective isn't more layers.
The objective is clear ownership.
Follow the Request Instead of Staring at the Diagram
Architecture diagrams can become abstract very quickly.
A better way to understand a system is to follow an actual request.
Imagine the user clicks:
Create Order
The request could travel through:
Client
│
▼
API
│
▼
Authentication
│
▼
Authorization
│
▼
Validation
│
▼
CreateOrderService
│
├── Product Validation
├── Inventory Check
├── Price Calculation
└── Order Creation
│
▼
Database Transaction
│
▼
Order Created
│
▼
Domain Event
│
├── Audit
├── Notification
└── Analytics
Now architecture becomes easier to reason about.
Every component has a purpose.
This gives us another useful rule:
If you cannot explain why a component exists in the request journey, question whether you need that component.
Turning Boundaries Into Code
Once the business boundaries are clear, we can map them into the project.
For example:
backend/
│
├── apps/
│ │
│ ├── identity/
│ │ ├── models/
│ │ ├── services/
│ │ ├── permissions/
│ │ └── api/
│ │
│ ├── organization/
│ │ ├── models/
│ │ ├── services/
│ │ ├── permissions/
│ │ └── api/
│ │
│ ├── inventory/
│ │ ├── models/
│ │ ├── services/
│ │ ├── repositories/
│ │ └── api/
│ │
│ ├── sales/
│ │ ├── models/
│ │ ├── services/
│ │ ├── repositories/
│ │ └── api/
│ │
│ ├── purchasing/
│ ├── billing/
│ └── reporting/
│
├── shared/
│ ├── authentication/
│ ├── authorization/
│ ├── exceptions/
│ ├── logging/
│ └── utilities/
│
├── infrastructure/
│ ├── database/
│ ├── cache/
│ ├── messaging/
│ ├── storage/
│ └── email/
│
└── config/
The important part isn't the exact folder names.
It's the reasoning behind them.
We didn't say:
"Every application needs repositories."
We first asked:
"What are the responsibilities?"
Then we created a structure that reflects those responsibilities.
What Belongs Inside a Module?
Let's zoom into sales.
A possible module might look like:
sales/
│
├── models/
├── services/
├── repositories/
├── permissions/
├── events/
├── validators/
└── api/
But folders alone don't create architecture.
Each part needs a purpose.
API
The API is the system's external boundary.
It handles:
HTTP Requests
Validation Input
Serialization
HTTP Responses
Status Codes
It translates external communication into application operations.
It should not become the entire business engine.
Application Services
Services should represent meaningful operations.
For example:
CreateOrderService
ConfirmOrderService
CancelOrderService
RefundOrderService
A useful question is:
What business operation does this service represent?
If the answer is unclear, the abstraction may not be useful.
Domain Logic
This is where business rules belong.
For example:
An order cannot be cancelled after shipment.
Inventory cannot become negative.
A finalized invoice cannot be edited.
Only an authorized organization owner can transfer ownership.
These aren't HTTP concepts.
They're business rules.
They should remain as independent from the delivery mechanism as practical.
Persistence
Persistence handles data access.
For example:
find_order()
find_product()
save_order()
get_inventory()
Its responsibility is retrieving and storing information.
It shouldn't decide:
"Is this customer allowed to cancel the order?"
That's a business decision.
Don't Turn Everything Into a Service
There's another architectural trap.
Once developers discover service classes, everything becomes a service:
UserService
ProductService
OrderService
EmailService
DatabaseService
ValidationService
HelperService
ManagerService
UtilsService
Soon the codebase has hundreds of abstractions.
But abstraction isn't automatically architecture.
The objective isn't:
More classes.
It's:
Clearer responsibilities.
Before introducing a service, ask:
What meaningful business capability or use case does this component represent?
Architecture should reduce cognitive load.
Not create another maze for developers to navigate.
The Database Is Not "Just Storage"
One of the biggest architectural mistakes is treating the database as a passive storage box.
For business systems, the database is part of the architecture.
Take inventory.
A simplistic model might store:
Product
quantity = 500
But where did those 500 units come from?
Maybe:
Purchase
Sale
Return
Transfer
Adjustment
Damage
If we only store the current number, we've lost the history.
A stronger model records inventory movements:
Product
│
▼
Inventory Movement
│
├── Purchase
├── Sale
├── Return
├── Adjustment
└── Transfer
Now the system can answer two very different questions:
How much inventory do we have?
and:
Why do we have this amount?
That second question becomes critical for:
- auditing
- reconciliation
- reporting
- debugging
- financial accuracy
So database architecture is more than:
API → Database
It is closer to:
Business Rules
↓
Data Model
↓
Transactions
↓
Constraints
↓
Indexes
↓
Persistence
A good data model doesn't merely store information.
It helps protect the business.
Transactions: Keeping Business Operations Consistent
Consider order creation.
We may need to perform:
Create Order
Reduce Inventory
Create Financial Record
Now imagine:
Order → Success
Inventory → Success
Financials → Failure
The system is inconsistent.
The order exists.
Inventory changed.
But the financial record didn't.
For operations that must succeed or fail together, we need a transactional boundary:
Transaction
│
├── Create Order
├── Update Inventory
└── Create Financial Record
Either:
Everything succeeds
or:
Everything rolls back
But there's an important architectural distinction.
Not every operation belongs inside the transaction.
Sending an email doesn't normally need to hold the database transaction open.
Generating a large PDF doesn't either.
Sending analytics data may not need to block the user.
That leads naturally to asynchronous processing.
Some Work Should Happen Later
Imagine a user creates an order.
Do they really need to wait while the system:
Send Email
Generate PDF
Update Analytics
Notify Warehouse
Sync External System
Probably not.
The user mainly needs one answer:
Was my order created successfully?
So the system can separate critical work from background work:
Create Order
│
▼
Database Transaction
│
▼
Order Created
│
▼
Return Response
Then:
Order Created Event
│
├── Send Email
├── Generate Invoice
├── Update Analytics
├── Notify Warehouse
└── Sync External System
Queues and workers become useful here.
But notice the reasoning.
We didn't begin with:
"Let's install RabbitMQ."
We began with:
"Which work should not block the user's request?"
That's the architectural decision.
Synchronous vs Asynchronous Processing
A simple rule of thumb:
Keep work synchronous when:
- the user needs the result immediately
- the operation is relatively fast
- immediate consistency matters
For example:
Create Order
Validate Payment
Update Inventory
Consider asynchronous processing when:
- the operation is slow
- the user doesn't need the result immediately
- it can be retried
- an external system is involved
- the work is computationally expensive
For example:
Generate Large Report
Send Bulk Emails
Export Millions of Records
Process Large Files
Synchronize External Data
The lesson isn't:
"Always use queues."
It's:
Choose synchronous or asynchronous processing based on the characteristics of the work.
Authentication Isn't Authorization
As the ERP grows, we need to know who is making a request.
That's authentication.
But identity alone isn't enough.
We also need to know what that person can do.
That's authorization.
A typical enterprise request might look like:
User
↓
Organization Membership
↓
Role
↓
Permissions
For example:
A manager might have:
invoice.view
invoice.create
inventory.view
inventory.adjust
While another employee may only have:
inventory.view
order.create
So remember:
Authentication asks:
Who are you?
Authorization asks:
What are you allowed to do?
Those are different architectural concerns.
Multi-Tenancy Changes Everything
Now let's turn our ERP into a SaaS platform.
Instead of one company, we have:
Organization A
Organization B
Organization C
...
Organization 500
The security question changes.
It is no longer simply:
Can this user access this order?
It becomes:
Can this user, inside this organization, perform this action on this resource?
The request might therefore follow:
Request
↓
Authenticate User
↓
Resolve Organization
↓
Verify Membership
↓
Authorize Action
↓
Query Tenant-Scoped Data
The database should reinforce this boundary.
For example:
Order
│
├── id
├── organization_id
├── customer_id
└── total
Now the order belongs to a specific organization.
This is more than a feature.
Tenant isolation is an architectural security boundary.
A failure here can expose one customer's information to another customer.
That's why multi-tenancy should be considered during architectural design, not bolted on later.
APIs Are Contracts, Not Just Endpoints
The internal architecture may change many times.
Clients shouldn't need to know about those internal changes.
A web application shouldn't care whether the backend uses:
PostgreSQL
Redis
Workers
A Monolith
Microservices
A Message Broker
It should communicate through a stable contract:
Web App
│
Mobile App
│
Partner
│
Internal Tool
│
▼
API
│
▼
Application
That's why API design matters.
An API is a contract between:
- clients
- developers
- internal teams
- partners
- future products
A strong API allows internal implementation to evolve without forcing every consumer to understand the internal architecture.
Then Reality Happens: Performance Problems
Eventually the ERP grows.
The dashboard becomes popular.
A request such as:
GET /dashboard
might perform:
20 database queries
3 aggregations
5 joins
2 external requests
Now thousands of users hit the same endpoint.
The database becomes a bottleneck.
The obvious response might be:
"Add more database servers."
But first ask:
Are we calculating the same information repeatedly?
If yes, caching might help.
Request
│
▼
Cache
│
├── Hit ──────► Response
│
└── Miss
│
▼
Database
│
▼
Store Cache
│
▼
Response
But caching creates new architectural questions:
How long should data remain cached?
When should it expire?
When should it be invalidated?
Is stale data acceptable?
What happens if the cache is unavailable?
What information is safe to cache?
So don't add a cache because:
"Scalable systems use Redis."
Add caching because:
A measured workload shows that caching solves a real problem.
Observability: When Production Starts Talking
Eventually someone will tell you:
"The API is slow."
Now what?
You need to answer:
Which endpoint?
Which request?
Which user?
Which organization?
How long did it take?
Which database query was slow?
Was it a cache miss?
Did the queue become overloaded?
Did an external service fail?
That's observability.
A useful request context might include:
request_id
user_id
organization_id
endpoint
status
duration
And the three familiar pillars are:
Logs
Metrics
Traces
Logs
Tell you what happened.
Metrics
Tell you how frequently and severely something is happening.
Traces
Show where time was spent across a request.
Without observability, production debugging becomes:
Something is slow.
Something is broken.
Maybe restart the server?
That's not an architecture strategy.
Infrastructure Comes After the Application
Only after understanding the application should infrastructure decisions become concrete.
A reasonable production starting point might look like:
Internet
│
▼
Load Balancer
│
▼
API Servers
/ | \
/ | \
▼ ▼ ▼
PostgreSQL Redis Object Storage
│
▼
Message Queue
│
▼
Workers
Around that system:
CI/CD
Monitoring
Logging
Tracing
Backups
Secrets Management
Alerts
Now each component has a reason.
PostgreSQL → business data
Redis → selected fast-access workloads
Object Storage → files
Message Queue → asynchronous coordination
Workers → background processing
Monitoring → system health
CI/CD → safe delivery
This is much healthier than creating a diagram with 25 technologies first and then trying to invent reasons for them.
Should You Start With Microservices?
Eventually, someone will probably say:
"We should use microservices."
Maybe.
But let's look at the context.
Suppose the company has:
5 Developers
1 Product
10 Customers
And we introduce:
User Service
Product Service
Order Service
Inventory Service
Billing Service
Notification Service
API Gateway
Service Mesh
Message Broker
Kubernetes
It looks sophisticated.
But now a simple feature might require:
Multiple Services
Multiple Deployments
Network Communication
Service Authentication
Distributed Debugging
Event Coordination
We've created a distributed system before we actually had a distributed problem.
The architecture is now more complicated than the business.
A simpler starting point could be:
Modular Monolith
│
┌────────────┼────────────┐
▼ ▼ ▼
Identity Inventory Sales
│ │ │
└────────────┼────────────┘
▼
PostgreSQL
Everything can still deploy together.
But the boundaries remain explicit.
That's important.
Simple deployment does not have to mean chaotic architecture.
Modular Monolith vs Microservices
A modular monolith doesn't mean:
"We will never use microservices."
It means:
"We will introduce distribution when distribution solves a real problem."
The evolution can look like:
Modular Monolith
│
▼
Identify Bottleneck
│
▼
Find the Right Boundary
│
▼
Extract One Capability
│
▼
Independent Service
Suppose reporting becomes extremely expensive.
Instead of splitting everything:
Identity
Sales
Inventory
Billing
Reporting
Notifications
we might extract only reporting:
Main Application
│
├── Identity
├── Sales
├── Inventory
└── Billing
+
Reporting Service
Now reporting can scale independently.
We didn't distribute the entire company.
We distributed the capability that actually needed it.
Architecture Should Have an Evolution Path
A system doesn't need its final architecture on day one.
It might begin as:
API
│
▼
Application
│
▼
PostgreSQL
Traffic increases:
API
│
▼
Application
│
├── PostgreSQL
└── Redis
Background workloads grow:
API
│
▼
Application
│
├── PostgreSQL
├── Redis
└── Workers
Asynchronous workflows become more complex:
API
│
▼
Application
│
├── PostgreSQL
├── Redis
├── Message Bus
└── Workers
Eventually, perhaps one capability becomes independently scalable:
API Gateway
│
┌───────────┼───────────┐
▼ ▼ ▼
Sales Inventory Billing
│ │ │
└───────────┼───────────┘
▼
Event Platform
The important part isn't the final diagram.
It's the journey between the diagrams.
Good architecture is an evolution path, not a perfect diagram created on day one.
Five Questions Before Adding Complexity
Before introducing another architectural component, ask five questions.
1. What problem are we solving?
Don't begin with:
"Should we use Kafka?"
Begin with:
"What problem are we experiencing?"
2. Who should own this responsibility?
Ask:
"Which module should own this behavior?"
Ownership prevents responsibility from leaking everywhere.
3. Does this need to happen immediately?
Ask:
"Does the user need the result before the request finishes?"
If not, asynchronous processing may be appropriate.
4. What happens as the system grows?
Ask:
"Could this component become a bottleneck or coupling point?"
5. What does this component cost operationally?
Every new component creates work.
A new service can mean:
Deployment
Monitoring
Networking
Authentication
Debugging
Failure Handling
Documentation
Ownership
Architecture isn't free.
Every abstraction has a maintenance cost.
Every distributed component creates another possible failure boundary.
So good architects don't only ask:
"Can we add this?"
They ask:
"Do we actually need this?"
The Architecture I'd Start With
If I were starting a SaaS or ERP platform today, I wouldn't begin with a giant distributed architecture.
I'd start closer to:
Client Applications
│
▼
API Layer
│
▼
Modular Application
│
┌───────────────┼───────────────┐
▼ ▼ ▼
Identity Inventory Sales
│ │ │
├───────────────┼───────────────┤
│ │ │
▼ ▼ ▼
Billing Purchasing Reporting
│
▼
PostgreSQL
│
┌─────┴─────┐
▼ ▼
Redis Workers
│
▼
Message Queue
Alongside it:
Authentication
Authorization
Tenant Isolation
Transactions
Structured Logging
Metrics
Tracing
CI/CD
Backups
And deliberately avoid introducing:
Microservices
Service Mesh
Kubernetes
Distributed Databases
Complex Event Platforms
until the product actually gives you a reason.
These technologies aren't bad.
The principle is simpler:
Complexity should be earned by the problem.
Production Architecture Checklist
Before calling an architecture production-ready, ask:
Business
- Do we understand the business capabilities?
- Are responsibilities clearly separated?
- Do modules represent meaningful business boundaries?
Application
- Is business logic separated from HTTP concerns?
- Are use cases easy to identify?
- Are dependencies understandable?
- Can modules evolve independently where appropriate?
Data
- Does the data model reflect business requirements?
- Are important invariants protected?
- Are transactions used correctly?
- Are indexes based on real query patterns?
- Is historical information preserved where necessary?
Security
- Is authentication handled consistently?
- Is authorization explicit?
- Is tenant isolation enforced?
- Are sensitive operations auditable?
- Are secrets protected?
API
- Are resources predictable?
- Are responses consistent?
- Are breaking changes controlled?
- Is documentation available?
- Is the API treated as a stable contract?
Performance
- Is pagination implemented?
- Are expensive queries optimized?
- Is caching used where justified?
- Can heavy work move to background processing?
Reliability
- Are failures handled?
- Can background jobs be retried safely?
- Are critical operations idempotent where necessary?
- Are backups available?
- Can the system recover from dependency failures?
Observability
- Can we trace requests?
- Can we measure latency?
- Can we identify errors?
- Can we monitor database performance?
- Can we understand queue and worker health?
Operations
- Can we deploy safely?
- Can we roll back?
- Are secrets managed securely?
- Are alerts configured?
- Can engineers understand what's happening in production?
If you can't answer these questions, the architecture probably isn't finished.
The Architecture Mindset
After all of this, software architecture can sound complicated.
But the underlying process is surprisingly simple.
When designing a system, keep asking:
What is this system responsible for?
Then:
Who should own that responsibility?
Then:
How should that responsibility communicate with other parts of the system?
Then:
What data does it need?
Then:
What happens when something fails?
And finally:
What happens when the business becomes larger?
Those questions naturally lead to:
Business Requirements
↓
Business Capabilities
↓
Boundaries
↓
Modules
↓
Application Logic
↓
Data Architecture
↓
API Contracts
↓
Transactions
↓
Async Processing
↓
Security
↓
Observability
↓
Infrastructure
↓
Deployment
That's architecture.
Not the number of services.
Not the number of technologies.
Not how complicated the architecture diagram looks.
Beginner vs Senior Architecture Thinking
A beginner often asks:
"Which architecture should I use?"
A developer asks:
"How should I structure this project?"
A senior engineer asks:
"Where should this responsibility live?"
A CTO asks:
"How will this architecture affect the business six months from now?"
A strong architect eventually learns to ask all four.
Because the goal isn't to build the most sophisticated system.
The goal is to build something that is:
understandable today, maintainable tomorrow, and capable of evolving when the business changes.
Final Thoughts: Architecture Is a Journey 🧠
A business idea doesn't become a production system by adding more technologies.
It becomes a production system through a series of deliberate decisions.
First:
Understand the Business
Then:
Find the Responsibilities
Then:
Create Boundaries
Then:
Define Ownership
Then:
Protect the Data
Then:
Design the Contracts
Then:
Handle Failure
Then:
Measure the System
And only then:
Introduce Complexity When Necessary
That's the real progression.
Good architecture isn't about having more components.
It's about putting the right responsibility in the right place.
And perhaps the simplest way to remember it is:
Don't start with technology.
Start with the business.
Find the responsibilities.
Create the boundaries.
Define ownership.
Protect the data.
Design the interactions.
Plan for failure.
Measure the system.
Scale what actually needs scaling.
Then introduce complexity only when the problem earns it.
That's how a simple business idea becomes a production-ready software architecture.
Key Takeaways
- Start architecture with business capabilities, not technologies.
- Organize systems around responsibilities and ownership.
- Use boundaries to prevent uncontrolled coupling.
- Keep HTTP concerns separate from core business rules.
- Treat the database as an architectural component.
- Use transactions to protect operations that must succeed together.
- Move appropriate workloads to asynchronous processing.
- Separate authentication from authorization.
- Treat tenant isolation as a security boundary.
- Design APIs as stable contracts.
- Add caching because a workload needs it, not because it's fashionable.
- Build observability before production becomes difficult to debug.
- Don't introduce microservices before you have a distributed problem.
- Prefer a modular monolith when it provides enough structure for the current stage.
- Design architecture as an evolution path, not a final diagram.
- Every architectural component has an operational and maintenance cost.
- Complexity should have a reason.
Top comments (0)