Evolution Of Software Design
Purpose
This page gives a short historical view of how software design evolved. The goal is not nostalgia. The goal is to help teams understand why today's architecture practices exist and why no style is universally "best."
Every design style emerged to solve a pressure:
- Programs became larger.
- Teams became larger.
- Systems needed reuse.
- Systems became distributed.
- Businesses needed integration.
- Domains became complex.
- Web APIs became the integration layer.
- Cloud made deployment, scaling, and operations central.
The useful lesson:
Architecture evolves when the cost of the previous style becomes too high.
Timeline Overview
| Era | Main Idea | What It Solved | What It Made Hard |
|---|---|---|---|
| Procedural | Organize code around procedures/functions | Straightforward control flow | Large codebases became tangled |
| Object-oriented | Organize code around objects and responsibilities | Encapsulation and modeling | Over-abstraction and inheritance complexity |
| Component-based | Package reusable units with contracts | Reuse and independent deployment units | Versioning and integration complexity |
| Distributed objects | Remote objects across machines | Location-transparent distributed calls | Network complexity was hidden too much |
| SOA | Business services with explicit contracts | Enterprise integration | Governance and heavyweight middleware |
| DDD and TDD | Domain model and test-driven feedback | Business complexity and design confidence | Requires discipline and collaboration |
| REST/GraphQL/Microservices | Web APIs and independently deployable services | Scale, team autonomy, integration | Distributed systems complexity |
| Containers and serverless | Package/runtime abstraction and event-driven compute | Deployment consistency and elastic scaling | Operational, cost, observability, and platform complexity |
1. Procedural Design
Procedural design organizes software as procedures or functions operating on data. The program is usually understood as a sequence of steps.
This style works well when:
- The problem is algorithmic.
- Data structures are simple.
- The program is small to medium.
- Flow is more important than modeling a business domain.
Procedural design gave software a clear mental model: do this, then this, then this. Many useful systems are still procedural at the right level. Scripts, data processing jobs, infrastructure automation, and command-line tools often benefit from simple procedural structure.
What went wrong at scale:
- Shared global data created hidden coupling.
- Large programs became difficult to change.
- Business rules were scattered across functions.
- Teams had difficulty owning boundaries.
Simpro lesson:
Procedural code is not bad. Procedural thinking becomes risky when business complexity needs clear ownership and boundaries.
Further study:
- Structured programming overview: https://en.wikipedia.org/wiki/Structured_programming
2. Object-Oriented Design
Object-oriented design organizes software around objects that combine data and behavior. The main promise was encapsulation: hide internal state and expose meaningful behavior.
This style became popular because software needed better ways to manage growing complexity. Objects allowed developers to model real-world or domain concepts such as Customer, Order, Invoice, Schedule, or Technician.
What it solved:
- Encapsulation.
- Responsibility assignment.
- Better domain modeling.
- Reuse through polymorphism.
- More maintainable large codebases when used well.
What went wrong:
- Inheritance trees became fragile.
- Objects became anemic data bags.
- Too many abstractions made simple code hard to follow.
- Teams confused "object-oriented" with "class-heavy."
Simpro lesson:
Object-oriented design is valuable when it expresses business behavior. It is harmful when it creates ceremony without clarity.
Further study:
- SOLID principles: https://en.wikipedia.org/wiki/SOLID
- Martin Fowler on refactoring: https://martinfowler.com/books/refactoring.html
3. Component-Based Design
Component-based design packages software into reusable units with clear interfaces. Components may be libraries, modules, packages, plugins, UI components, or deployable binaries.
The pressure behind this era was reuse. Teams did not want to rebuild everything from scratch. They wanted stable, replaceable building blocks.
What it solved:
- Reuse.
- Encapsulation at package/module level.
- Clearer contracts.
- Independent ownership of parts.
- UI and platform reuse.
What it made hard:
- Versioning.
- Dependency management.
- Binary compatibility.
- Component lifecycle.
- Over-generalized reusable components.
Simpro lesson:
Reuse is useful only when the reused component has a stable purpose. Premature reuse creates a shared dependency nobody enjoys changing.
Further study:
- Component-based software engineering: https://en.wikipedia.org/wiki/Component-based_software_engineering
- Microsoft COM overview: https://learn.microsoft.com/en-us/windows/win32/com/the-component-object-model
4. Distributed Technology: DCOM, CORBA, RMI And Others
As networks became common, the next dream was distributed objects: use objects across machines as if they were local.
Technologies included:
- DCOM from Microsoft.
- CORBA from OMG.
- Java RMI.
- RPC-based systems.
The idea was attractive: call remote functionality using familiar object-style programming.
What it solved:
- Cross-process and cross-machine integration.
- Reuse of remote business functionality.
- Enterprise component distribution.
What went wrong:
- Network calls are not local calls.
- Latency, partial failure, retries, timeouts, serialization, versioning, and security became unavoidable.
- Location transparency hid important operational realities.
This era taught one of architecture's most important lessons:
The network is real. Do not design as if remote calls are local method calls.
Simpro lesson:
When we distribute a system, we must design for failure, latency, observability, and contracts from the beginning.
Further study:
- Microsoft COM: https://learn.microsoft.com/en-us/windows/win32/com/the-component-object-model
- Microsoft DCOM Remote Protocol: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/
- CORBA overview: https://www.omg.org/spec/CORBA/
5. Service-Oriented Architecture
SOA emerged as enterprises needed integration across many applications. Instead of distributed objects pretending to be local, SOA emphasized services with explicit contracts.
SOA often used:
- SOAP.
- WSDL.
- Enterprise service buses.
- Service registries.
- XML-based contracts.
What it solved:
- Enterprise integration.
- Business capability reuse.
- Explicit service contracts.
- Cross-platform communication.
What went wrong:
- Heavy governance.
- Complex middleware.
- Slow change cycles.
- Centralized ESB bottlenecks.
- Services sometimes became technical wrappers instead of business capabilities.
Simpro lesson:
SOA's useful idea is business capability boundaries. Its warning is that too much centralized governance can slow learning.
Further study:
- SOA overview: https://en.wikipedia.org/wiki/Service-oriented_architecture
- W3C Web Services Architecture: https://www.w3.org/TR/ws-arch/
6. DDD And TDD
Domain-Driven Design and Test-Driven Development addressed two different but related needs: business complexity and design confidence.
Domain-Driven Design
DDD focuses on modeling complex business domains. It introduced language and patterns such as:
- Ubiquitous language.
- Bounded context.
- Entities.
- Value objects.
- Aggregates.
- Repositories.
- Domain services.
- Context maps.
DDD helps when business rules are complex and language matters. It is especially useful when different parts of the business use the same words differently.
Simpro example:
The word "job", "schedule", "customer", or "resource" may mean different things in different workflows. DDD helps teams make those meanings explicit.
Test-Driven Development
TDD uses tests as a design feedback mechanism. The classic loop is:
Red -> Green -> Refactor
TDD helps teams clarify expected behavior before implementation grows too large.
What these solved:
- Domain complexity.
- Shared language.
- Better boundaries.
- Safer refactoring.
- Design feedback.
What can go wrong:
- DDD becomes pattern-heavy without domain understanding.
- TDD becomes mechanical test writing without design thinking.
- Teams test implementation details instead of behavior.
Simpro lesson:
DDD helps us model the business. TDD helps us design behavior in small, testable steps. Both require thinking, not ritual.
Further study:
- Domain-Driven Design Reference by Eric Evans: https://www.domainlanguage.com/ddd/reference/
- DDD community resources: https://github.com/ddd-crew
- Test-driven development: https://martinfowler.com/bliki/TestDrivenDevelopment.html
7. REST, GraphQL And Microservices
The web made HTTP the dominant integration layer. REST popularized resource-oriented APIs. GraphQL gave clients a way to query exactly the data they need. Microservices pushed the idea of independently deployable services organized around business capabilities.
REST
REST works well when resources and standard HTTP semantics fit the problem.
Strengths:
- Simplicity.
- HTTP-native.
- Cache-friendly.
- Broad tooling.
Common issue:
- APIs become inconsistent when resource modeling is weak.
GraphQL
GraphQL works well when clients need flexible data queries across a graph of related data.
Strengths:
- Client-driven queries.
- Reduces over-fetching and under-fetching.
- Strong schema.
Common issue:
- Query complexity, authorization, caching, and operational controls require discipline.
Microservices
Microservices work well when independent deployment, team autonomy, and business capability boundaries are more valuable than simplicity.
Strengths:
- Independent deployability.
- Team autonomy.
- Scaling by service.
- Technology flexibility.
Common issue:
- Distributed complexity, data consistency, observability, testing, and deployment coordination.
Simpro lesson:
Do not choose microservices because they sound modern. Choose them when the organizational and domain boundaries justify the operational cost.
Further study:
- Martin Fowler on REST: https://martinfowler.com/articles/richardsonMaturityModel.html
- GraphQL official documentation: https://graphql.org/learn/
- Martin Fowler microservices guide: https://martinfowler.com/microservices/
- Chris Richardson microservices patterns: https://microservices.io/patterns/index.html
8. Containers And Serverless
Containers and serverless changed how software is packaged, deployed, and scaled.
Containers
Containers package applications with their runtime dependencies, making deployment more consistent across environments.
What they solved:
- "Works on my machine" environment drift.
- Packaging consistency.
- Easier scaling and orchestration.
- Better local-to-production parity.
What they introduced:
- Image management.
- Container security.
- Orchestration complexity.
- Kubernetes learning curve.
- Observability and networking complexity.
Serverless
Serverless lets teams run code without managing servers directly. It is often event-driven and scales automatically.
What it solved:
- Infrastructure management for certain workloads.
- Event-driven scaling.
- Pay-per-use economics.
- Faster delivery for small functions and integrations.
What it introduced:
- Cold starts.
- Vendor coupling.
- Debugging complexity.
- Distributed tracing needs.
- Cost surprises at scale.
Simpro lesson:
Containers and serverless reduce some infrastructure work, but they do not remove architecture responsibility. We still need observability, security, cost control, and clear ownership.
Further study:
- Docker documentation: https://docs.docker.com/
- Kubernetes documentation: https://kubernetes.io/docs/home/
- CNCF cloud native definition: https://github.com/cncf/toc/blob/main/DEFINITION.md
- AWS Lambda overview: https://aws.amazon.com/lambda/
- Microsoft Azure Functions overview: https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview
The Big Pattern
Each era moved the boundary of design:
| Earlier Focus | Later Focus |
|---|---|
| Functions | Responsibilities |
| Objects | Components |
| Components | Services |
| Local calls | Network contracts |
| Technical boundaries | Business boundaries |
| Manual deployment | Automated platforms |
| Servers | Containers and functions |
| Code only | Code plus operations |
Modern architecture is not one style. It is choosing the right boundaries for the problem.
Simpro Guidance
Use this decision lens:
- If the problem is simple and local, keep it simple.
- If business language is complex, use DDD thinking.
- If behavior is risky, use tests to drive clarity.
- If integration is needed, design explicit contracts.
- If teams need independent delivery, consider service boundaries.
- If deployment friction repeats, use containers and platform automation.
- If workloads are event-driven and variable, consider serverless.
- If distribution adds more cost than value, keep the system together.
The best architecture is not the most modern architecture. It is the architecture that makes change safe, understandable, and valuable.
Team Reference Guide
How To Explain This Page
Use this page as a reference conversation, not as a checklist to read aloud. Start by explaining why the topic matters, then connect it to current team work, and finally ask what behavior should change.
The most useful way to teach this material is to move from concept to example. Explain the principle, show how it appears in daily work, ask the team where it is currently strong or weak, and finish with one small action.
Guidelines For Teams
- Connect the topic to a current project, customer problem, incident, or decision.
- Translate concepts into visible behaviors.
- Keep the guidance lightweight enough to use weekly.
- Capture decisions, examples, and improvements back into the wiki.
- Review the page again after a project, incident, or retrospective to update what the team has learned.
Reflection Questions
- What part of this topic is already working well for us?
- What part is still mostly theory?
- What is one behavior we can change in the next 30 days?