The Evolution of Software

How we got here, what went wrong, and a simpler path forward

A Brief History

Software has evolved through stages. Each solved a real problem, but introduced new challenges that led to the next stage.

timeline TD
    1980s - Desktop : Simple self-contained apps on a single computer
    1990s - Client-Server : Networks and databases enable sharing
    2000s - Web & Services : The Internet solves solves app distribution
    2010s - Microservices : Manage complexity by splitting into smaller pieces
    Today - Event-Driven : A fundamentally different way to communicate

1980s: Desktop - Everything on One Machine

The user interface, logic, and data all lived on one computer.

flowchart TD
  subgraph PC["Single Computer"]
    direction TB
    UI[User Interface]
    Files@{ shape: docs }
  end
  UI:::basic --> Files:::db

What worked

  • Simple and fast
  • No dependencies

What didn't

  • One user at a time
  • To share data users copied it to floppy disk and gave it to another user (sneaker net)
  • Duplicate and divergent copies of the same data
  • Data loss when a disk is damaged

1990s: Client-Server - Networks and Databases

Networks and shared databases solved isolation. Multiple users, same data in a 2-tier architecture.

flowchart TD
  A[Desktop App]:::basic & B[Desktop App]:::basic & C[Desktop App]:::basic --> DB[(Shared Database)]:::db

What this solved

  • Shared data with real-time visibility
  • Central management and backups

What it didn't

  • Updating apps meant visiting every machine

The trust problem: Every desktop had direct database access - minimal security boundary

2000s: The Web and Services

The Web browser and the Internet solved the app distribution problem, but as networks opened to the world a new trust layer had to be introduced to secure data and protect its integrity.

The solution: a service layer that authorizes, validates, and enforces data integrity rules. This is the 3-tier architecture most apps still use.

flowchart LR
  Browser[Browser]:::basic -->|Internet| Service[Service Layer] -->|Private| DB[(Database)]:::db

Works well for one app. But what happens when you have many?

The Challenge: Many Apps

Modern organizations run many systems: Website, Customer Relationship Management (CRM), Enterprise Resource Planning (ERP), warehouse, partner portal, and more.

Each needs data from the others.

The natural approach: connect them directly.

The problem isn't any single connection - it's what happens as systems grow...

flowchart
  A[Website]:::basic <--> B[ERP]:::basic
  A <--> C[CRM]:::basic
  A <--> D[Warehouse]:::basic
  B <--> C
  B <--> D
  B <--> E[Partner Portal]:::basic
  C <--> D
  C <--> E
  D <--> E

How Connections Grow

2 systems - 1 connection

flowchart LR
  A[Website]:::basic <--> B[CRM]:::basic

3 systems - 3 connections

flowchart LR
  A[Website]:::basic <--> B[CRM]:::basic
  A <--> C[ERP]:::basic
  B <--> C

4 systems - 6 connections

flowchart LR
  A[Website]:::basic <--> B[CRM]:::basic
  A <--> C[ERP]:::basic
  A <--> D[Warehouse]:::basic
  B <--> C
  B <--> D
  C <--> D

5 systems - 10 connections

flowchart LR
  A[Website]:::basic <--> B[CRM]:::basic
  A <--> C[ERP]:::basic
  A <--> D[Warehouse]:::basic
  A <--> E[Portal]:::basic
  B <--> C
  B <--> D
  B <--> E
  C <--> D
  C <--> E
  D <--> E

8 Systems - 28 Connections

flowchart LR
  A[Website]:::basic <--> B[ERP]:::basic
  A <--> C[CRM]:::basic
  A <--> D[Warehouse]:::basic
  A <--> E[Portal]:::basic
  A <--> F[Mobile]:::basic
  A <--> G[Reports]:::basic
  A <--> H[Billing]:::basic
  B <--> C
  B <--> D
  B <--> E
  B <--> F
  B <--> G
  B <--> H
  C <--> D
  C <--> E
  C <--> F
  C <--> G
  C <--> H
  D <--> E
  D <--> F
  D <--> G
  D <--> H
  E <--> F
  E <--> G
  E <--> H
  F <--> G
  F <--> H
  G <--> H

Every pair connected. Every connection is custom code that must be designed, built, tested, and maintained.

The Math of Direct Connections

Systems Connections
2 1
3 3
4 6
5 10
8 28
10 45
15 105
20 190

Formula: n(n-1)/2

Each connection is custom code that must be:

  • Designed - how will systems exchange data?
  • Built - someone writes the code
  • Tested - someone manually tests or writes automated tests
  • Deployed - coordinated with both systems
  • Monitored - is it still working?
  • Maintained - new software versions and security updates need constant changes that each require repeating the build, test, and deploy steps

2010s: Microservices - Build Smaller to Reduce Complexity

"We try to create teams that are no larger than can be fed by two pizzas."

— Jeff Bezos (Amazon CEO): AWS Whitepaper: Introduction to DevOps on AWS

Following this philosophy, the industry broke apps into small services each owned by a small team.

Example

The website works with one service for searching and listing products, another for pricing, and another to process orders - each specific for one purpose.

The promise

  • Easier to build, test, deploy, maintain, and scale small services
  • Small independent teams can move faster
  • Better ownership and accountability

Unintended consequences

  • Microservices multiply connections
  • Problems cascade between systems
  • Debugging problems across systems was harder
  • Deployment still required coordination

Microservices didn't eliminate complexity, they just moved it. And added more connections!

The Communication Problem

When one system in the chain fails or is slow, everything upstream is affected.

Direct Communication

  • A waits for B - if B is slow, A is slow
  • If B fails, A fails - user sees an error
  • If B changes its API or data, A breaks

This is called tight coupling. The systems can't function independently.

flowchart LR
  A[Website]:::basic --> B[Order Service] --> C[Payment Service]
  style C fill:#fef5f5,stroke:#c0392b
  style B fill:#fef5f5,stroke:#c0392b
  style A fill:#fef5f5,stroke:#c0392b

Imagine what happens in a complex web of connections

In a worse case scenario if any system goes down, the entire system is broken!

The Scaling Problem

Each app also faces another challenge - reads and writes are fundamentally different operations competing for the same resources.

Queries: Reads often 90%+ of traffic

  • Browsing, searching, reporting
  • Need speed and flexibility
  • Many different views of the same data

Commands: Writes are infrequent

  • Creating, updating records
  • Need validation and reliability
  • Must enforce business rules

Traditional architecture:

  • Cannot independently optimize and scale for both fast reads and writes
  • Reads need different data from writes
  • Read and write security is more complex
  • Shared data tables and locked records can significantly reduce overall performance

If queries/reads and commands/writes have different needs, why not separate them? This pattern is called CQRS - Command Query Responsibility Segregation.

A Different Approach

How can we solve these problems and make systems less expensive to build?

CQRS - Separating Reads from Writes

To solve the scaling problem, we separate commands from queries - each gets a dedicated path.

flowchart LR
  App[App]:::basic -->|Writes| C[Command] --> DB[(Database)]:::db --> Q[Query] -->|Reads| App

Command (Write Path)

Receives, validates, and stores data. Must be reliable.

Query (Read Path)

Reads data back out, optimized for speed. Must be fast and flexible.

Each path can be optimized:

  • Separate data designs for reads and writes
  • Independent read and write scaling allow both to be fast
  • Simplified security and data designs

This solves scaling within one app - but what about the connection problem?

2020s: Event-Driven - Communicating Through a Message Broker

Every system communicates through a message broker whose only job is to reliably deliver messages.

flowchart TD
  A[Website] <--> MB([Message Broker]):::basic
  B[CRM] <--> MB
  MB <--> C[ERP]
  MB <--> D[Warehouse]

Promises fulfilled

  • If a service goes down, it is the only one affected and its messages wait
  • New systems don't affect existing ones
  • Different teams, schedules, technologies

Each system has one connection - to the message broker. A 50th system? Still one connection.

Systems notify each other of changes through events.

The Connection Math, Revisited

Systems Direct Broker
2 1 2
3 3 3
4 6 4
5 10 5
8 28 8
10 45 10
15 105 15
20 190 20

With direct connections complexity grows with a square function (specifically: n(n-1)/2).

Broker connections grow linearly as n.

This doesn't just make things simpler - it changes the scaling equation entirely.

Even if you don't have many systems today, it is almost inevitable that they will increase in time.

CQRS Meets the Broker

Adding the broker also introduces a third component: the projection.

flowchart LR
  App[App]:::basic --> C[Command] --> MB([Broker]):::basic --> P[Projection] --> DB[(Database)]:::db --> Q[Query] --> App

Three custom components

  • Command - An API to receive data, validate, and publish messages to the broker. Must be reliable.
  • Projection - Listens to the broker, extracts data from messages, and writes it into the database. Must be consistent.
  • Query - An API for the frontend to retrieve data from the database. Must be fast and flexible.

These aren't separate decisions - they're the natural shape of a backend when you use a broker.

  • Each component can be scaled independently
  • Each can be optimized for its specific purpose
  • The broker handles delivery, retries, and durability

Every App Becomes Independent

Each app has its own backend components and database - they are only connected through the message broker.

  • Independence - Different teams can develop, deploy, update each app separately and in parallel
  • Resilience - One app down? Others keep running uninterrupted. An app catches itself up when it comes back online.
  • Simplicity - Each app only deals with its own database and concerns
flowchart TD
  C1[Command 1] & C2[Command 2] & C3[Command 3] --> MB([Message Broker]):::basic
  MB --> P1[Projection 1] & P2[Projection 2] & P3[Projection 3]
  P1 --> DB1[(Database 1)]:::db --> Q1[Query 1]
  P2 --> DB2[(Database 2)]:::db --> Q2[Query 2]
  P3 --> DB3[(Database 3)]:::db --> Q3[Query 3]

Every App Becomes Simpler

When apps are decoupled, each one gets simpler on its own.

Before

  • Tight coupling of apps and data
  • Lots of code for:
    • Data format conversions between systems
    • Retry logic, timeouts, circuit breakers
    • Error handling for unavailable systems
  • Coordinated deployments and schedules

After

  • Simple write to the broker
  • Data is app-owned and read-optimized
  • Easier to change:
    • No knowledge of other systems
    • No coordinated deployments
  • App focuses on logic and user experience
  • Team independence

Decoupling makes each app easier to understand, cheaper to build, and simpler to maintain

Where Developer Time Actually Goes

A least 70% of time, energy, and dollars goes into undifferentiated, heavy lifting.

— Jeff Bezos (Amazon CEO), 2013: Opening Keynote

Developers spend 17.3 hours per week (42%) dealing with maintenance issues and 13.5 hours per week (33%) addressing technical debt.

— Stripe + Harris Poll, 2018: The Developer Coefficient (PDF)

About one fifth (20%) of tech budgets goes toward creating new value-generating business models or entering new markets.

— Deloitte Insights, 2023: Strategies for allocating capital and articulating value

16% of time goes to developing applications and 14% to writing requirements and test cases.

— IDC Report, 2025 - Developers Spend Most of Their Time Not Coding

The majority of effort goes to infrastructure that follows the same patterns over and over

The Pattern Is Clear

For each component of every app, this work must be handled:

All Components

Health checks, monitoring, logging, tracing, deployment, security

Query

Read endpoints, filtering, sorting, paging, frontend coordination

Command

Write endpoints, validation, data mapping and transformation, publish guarantees

Projection

Broker subscription, data extraction, transformation, mapping, duplicate and retry handling, transactions, outgoing notifications

The dilemma: The broker-based architecture is clearly better, but the upfront cost of building three backend components makes teams hesitate. Many fall back to direct integrations because they're faster to start, even knowing they'll pay for it later in maintenance and fragility.

LiCQuid

The Command, Query, and Projection components - already built.
Configure with JSON. Deploy as containers. Start building your app.

Putting the flow in CQRS.

Configuration, Not Code

LiCQuid components are configured with JSON. Describe what you want - LiCQuid handles the how.

The development workflow

  1. Design your database
  2. Configure LiCQuid
  3. Deploy the LiCQuid containers
  4. Start building your frontend

You define

  • Endpoints, validation rules, data mapping, connections

You don't need to build

  • Health checks, logging, or tracing
  • Command, Query, or Projection components
  • APIs, routing, paging, sorting, filtering
  • Message serialization
  • Database read/write code
  • Retry logic and error handling

When requirements change, update the configuration - there is no code to compile, test, and deploy.
The frontend team can query immediately - no waiting for backend engineers to change and deploy code.

LiCQuid.Command - Receive & Validate

Replaces a custom Command API you would otherwise build for each app:
flowchart LR
  App[App]:::basic -->|POST| C[LiCQuid.Command]
  C -->|Publish| MB([Broker]):::basic
  • API endpoints and routing
  • Input validation per message type
  • Broker connection, publishing, and error handling
  • Message transformation and formatting
  • Health checks, logging, tracing

LiCQuid.Command includes:

  • REST endpoints defined in JSON
  • Validation via standard JSON Schema
  • Idempotency and deduplication handling
  • Standard error responses
  • JSONata transformation templates
  • Health checks, logging, tracing
  • Brokers: Kafka, Event Hubs, RabbitMQ, or Service Bus

LiCQuid.Projection - Transform & Store

Replaces the custom message processing service you would otherwise build:

flowchart LR
  MB([Broker]):::basic -->|Message| P[LiCQuid.Projection]
  P -->|Write| DB[(Database)]:::db
  P -->|Notify| N([Broker]):::basic
  • Message parsing and field extraction
  • Mapping data to tables and columns
  • Database transactions
  • Upsert logic (create vs. update decisions)
  • Downstream event publishing
  • Health checks, logging, tracing

LiCQuid.Projection includes:

  • Simple topic subscriptions
  • JSONPath and regex data extraction
  • Multi-table writes in a single transaction
  • Automatic upsert handling
  • Conditional downstream notifications
  • Health checks, logging, tracing
  • Brokers: Kafka, Event Hubs, RabbitMQ, or Service Bus
  • Databases: SQL Server, PostgreSQL, MySQL, SQLite, Oracle

LiCQuid.Query - Auto-Generated Read API

Replaces the custom Query API you would otherwise build, and rebuild/redeploy every time the frontend needs something new:

flowchart LR
  App[App]:::basic -->|Read| Q[LiCQuid.Query]
  Q -->|Read| DB[(Database)]:::db
  • Read endpoints for each screen or data need
  • Database queries with filtering, sorting, paging
  • Frontend/backend team coordination
  • Extra rebuild and redeploy when things change
  • Health checks, logging, tracing

LiCQuid.Query includes:

  • Complete GraphQL API generated at startup
  • Filtering, sorting, paging
  • Frontend queries exactly what it needs
  • Batch multiple requests in a single call
  • Schema changes reflected on restart
  • Databases: SQL Server, PostgreSQL, MySQL, SQLite, Oracle
  • Bonus: Quick UI iterations

Before: UI needs change → ask backend team → backend builds endpoint → backend deploys → Finally available for UI use. Many times per screen.

After: UI needs change → UI makes change → works immediately. Done.

Common Starting Points

New App

You're building a new app and want a clean, scalable architecture.

flowchart TD
  C[LiCQuid.Command] --> B([Broker]):::basic --> P[LiCQuid.Projection] --> D[(Database)]:::db --> Q[LiCQuid.Query]

Legacy Integration

You have an existing system, but you need new modern apps.

flowchart TD
  L[(Legacy)]:::legacy --> B([Broker]):::basic --> P[LiCQuid.Projection] --> D[(New Database)]:::db --> Q[LiCQuid.Query]

Multiple Systems

You have multiple systems that need to share data reliably without building direct connections between each pair.

flowchart LR
  B([Broker]):::basic
  B --> P1[LiCQuid.Projection] --> D1[(Database 1)]:::db
  B --> P2[LiCQuid.Projection] --> D2[(Database 2)]:::db
  B --> P3[LiCQuid.Projection] --> D3[(Database 3)]:::db

Configure and deploy LiCQuid. Build your frontend.

Configure and deploy. Let LiCQuid help you replicate data. Build your frontend. Legacy untouched.

Each system gets its own database. Adding new ones doesn't affect others.

Looking Back, Looking Forward

Era What it solved What it introduced
Desktop Simple, self-contained apps Data trapped on one machine
Client-Server Shared data across users No trust boundary
Web / 3-Tier Security, access from anywhere Custom services for every app
Microservices Smaller, independent deployments Connection explosion
Event-Driven / CQRS Decoupled systems; resilience Building the infrastructure

LiCQuid removes the last barrier: the cost of building the infrastructure.
Your team focuses on the app. LiCQuid handles the rest.

Next Steps

We'd like to show you how this applies to your environment.

1. Walk through your current architecture
2. See a live demo
3. Discuss where LiCQuid fits

References

This worked inside a trusted office, but what happens when the network extends beyond the building?

This assumes each app pair only needs one type of integration. In practice, the number of connections between apps varies from zero to many.

The intention was good: smaller pieces should be easier to build, test, deploy, maintain, and scale independently. And for many teams this proved true to some extent - smaller and more independent teams are more productive and can move faster. But for the system as a whole, something else happened. Breaking one app into five services didn't reduce complexity - it redistributed it from one system to many and to the connections between services. Instead of simple calls within a single codebase, you now have network calls between services, each of which can fail, slow down, or return unexpected results.

Example: One long running query can consume most database resources and cause the entire system to perform so slow as to become unusable. If records are locked then queries can block all writes from processing.

Think of it like a post office. You don't need to know where the recipient lives or whether they're home. You drop off your letter, and the message broker (post office) makes sure it gets delivered. If the recipient is away, the letter waits until they return (they are back online).

pause

pause

pause