ISP/CRM Modernization
Re-engineering a production ISP management platform from a legacy PHP system into a testable, domain-oriented reference architecture
Original system: 2014
Re-engineered: 2026
Category: Enterprise Software / Legacy Modernization / Backend Engineering
Reference implementation: Laravel 12
Database: PostgreSQL
Architecture: Modular Monolith / Domain-Oriented Architecture
Testing: 836 Automated Tests / 7,274 Assertions
Project Overview
In 2014, I developed one of my major software projects for an Internet Service Provider.
The system was not a simple website or administrative dashboard.
It was an operational ISP/CRM platform responsible for managing a significant part of the company's day-to-day business processes, including customers, services, contracts, billing, payments, support, communications and operational workflows.
At the time, the application was built primarily with traditional PHP.
The complete system was remarkably compact:
- approximately 23 MB
- around 104 core PHP files
- individual files containing up to approximately 3,000 lines of code
That architecture was influenced by the technical and operational environment of the time.
The software needed to be deployed quickly, remain lightweight and operate inside an infrastructure where external access was heavily restricted at the network and firewall level.
For 2014, the system was practical.
Twelve years later, however, my definition of a successful software system had changed significantly.
The question was no longer simply:
Does the software work?
The new questions were:
Can its behavior be precisely defined?
Can its business rules be independently verified?
Can financial operations be protected by explicit invariants?
Can authorization decisions be tested rather than assumed?
Can the architecture evolve without silently changing existing behavior?
Can another engineer understand why the system behaves the way it does?
Those questions became the foundation of the 2026 modernization.
The Original 2014 System
The original application followed a development style that was common and practical for many PHP applications of that period.
Responsibilities that would normally be separated in a modern architecture often existed close together.
A single application flow could involve:
- SQL queries
- business rules
- authentication
- authorization
- validation
- HTML rendering
- form processing
- session management
- financial calculations
- customer operations
- service operations
In some cases, several of these concerns existed within the same PHP file.
This reduced abstraction overhead and helped keep the application compact.
It also allowed features to be implemented quickly.
But as the system grew, the same characteristics created long-term architectural limitations.
Large files became responsible for multiple business concerns.
Database operations were closely connected to application behavior.
Business rules were sometimes implicit in execution flow rather than explicitly modeled.
Authorization behavior was distributed across different parts of the system.
Testing individual rules independently was difficult.
The architecture worked, but much of the system's knowledge existed inside the implementation itself.
Why I Revisited the Project
When I returned to the project in 2026, I deliberately avoided treating the modernization as a framework upgrade.
Replacing old PHP syntax with modern PHP would not solve the architectural problem.
Moving existing code into a newer framework without understanding its behavior would simply move technical debt into a newer directory structure.
So I approached the project differently.
The modernization started with:
reverse engineering the behavior of the legacy system.
The objective was to identify:
- business entities
- business rules
- workflows
- state transitions
- authorization requirements
- financial invariants
- side effects
- integrations
- data relationships
- historical behavior
Only after understanding those concepts could the system be reconstructed in a modern architecture.
From Legacy Code to Domain Model
The legacy application contained years of operational knowledge.
That knowledge needed to be extracted from implementation details and converted into explicit domain concepts.
The modernized system therefore separates major business areas such as:
- CRM
- Customer Management
- Customer Contacts
- Contracts
- Service Plans
- Service Lifecycle
- Service Lines
- Service Cases
- Service Case Transitions
- Invoices
- Payments
- Financial Ledger
- Support
- Messaging
- SMS
- Telegram Integration
- Import & Reconciliation
- Authentication
- Authorization
- RBAC
- Audit Logging
- Customer Portal
- Idempotency
- API Contracts
Instead of allowing these concepts to exist implicitly across unrelated files, they are modeled as identifiable parts of the system.
This makes boundaries clearer and allows individual business rules to be tested independently.
Architecture
The 2026 reference implementation is based on Laravel 12 and PostgreSQL.
However, Laravel is not being used merely as a modern replacement for the original PHP application.
The reference implementation has a more important role:
It acts as an executable specification of the system's expected behavior.
This means the implementation does not only contain working features.
It also defines, through code, constraints and automated tests, what the system is allowed and not allowed to do.
The architecture follows a modular, domain-oriented approach.
Instead of organizing the entire system primarily around controllers and database tables, business capabilities are treated as architectural concepts with explicit responsibilities.
The result is a system that is easier to reason about, test and maintain.
The Reference Implementation
One of the most important milestones of the modernization was reaching a frozen Laravel reference state.
"Frozen" in this context means that the implementation has reached a defined behavioral baseline.
The reference contains:
- database schema
- domain rules
- authorization behavior
- API contracts
- validation rules
- expected state transitions
- test scenarios
- reference datasets
- architectural documentation
The purpose is to make system behavior explicit.
Instead of relying on statements such as:
"This is how the old system probably worked."
the project can express:
"This is the behavior that the reference implementation defines and verifies."
That distinction is fundamental to the modernization strategy.
Customer and CRM Domain
Customer management is one of the central domains of the platform.
A customer is not represented merely as a row containing a name and telephone number.
The model supports relationships between concepts such as:
- customers
- contacts
- services
- service lines
- credentials
- contracts
- invoices
- payments
- support cases
- communications
The objective is to prevent unrelated business responsibilities from accumulating inside a single customer record or controller.
Customer identity and customer-related operations therefore become explicit parts of the domain model.
Service Lifecycle
Internet services naturally have a lifecycle.
A service can move through different operational states over time.
These transitions may affect:
- activation
- suspension
- cancellation
- credentials
- billing
- technical provisioning
- customer communication
A major architectural improvement was to make these transitions explicit.
Instead of directly modifying status fields from arbitrary parts of the application, transitions can be modeled as business operations.
This creates an important distinction:
A state describes where an entity currently is.
A transition describes how and why it moved there.
That difference improves auditability and makes invalid transitions easier to prevent.
Financial Architecture
Financial behavior is one of the areas where implicit application logic creates the highest risk.
The modernized architecture therefore treats financial operations as explicit domain behavior.
The system models concepts including:
- invoices
- payments
- financial entries
- reconciliation
- financial state transitions
A particularly important principle is the use of an:
Immutable Financial Ledger
Once a financial event has been committed to the ledger, normal application operations should not silently rewrite its history.
Corrections should be represented through explicit compensating or subsequent operations rather than destructive modification of historical financial records.
This improves:
- traceability
- auditability
- reproducibility
- financial consistency
It also creates strong invariants that can be tested automatically.
Idempotency
Idempotency became another explicit architectural concern.
Consider a payment request.
A client sends the request.
The server processes the payment successfully.
Before the response reaches the client, the network connection fails.
The client retries.
Without idempotency protection, the second request could create another financial transaction.
The modernized system therefore treats retry safety as part of the business contract.
Equivalent requests using the same idempotency context must not create duplicate financial side effects.
This is especially important for:
- payments
- imports
- external integrations
- asynchronous operations
- retryable API requests
Idempotency is therefore tested as behavior rather than documented merely as an implementation recommendation.
Authentication and Authorization
Authentication answers:
Who is the user?
Authorization answers:
What is this user allowed to do?
The modern architecture treats these as separate concerns.
A Role-Based Access Control model was introduced with concepts such as:
- roles
- permissions
- role-permission relationships
- user-role relationships
- user-specific permission overrides
This allows authorization to be expressed explicitly rather than scattered across controllers and views.
The test suite also verifies authorization boundaries.
It is not sufficient that an authorized user can perform an operation.
The system must also demonstrate that an unauthorized user cannot perform it.
Tenant and Customer Data Isolation
Authorization is particularly important when customer-specific data is involved.
An authenticated user should not automatically gain access to unrelated customer information.
The architecture therefore treats object-level authorization and data isolation as explicit security requirements.
Tests verify scenarios where users attempt to access resources outside their permitted scope.
This helps prevent one of the most dangerous classes of application vulnerabilities:
valid authentication combined with invalid resource access.
Audit Logging
Enterprise systems need more than application logs.
They need to answer questions such as:
- Who performed an operation?
- What operation was performed?
- Which entity was affected?
- When did it happen?
- What was the result?
The modernized platform therefore includes explicit audit behavior.
At the same time, auditability must not become a mechanism for leaking sensitive information.
The audit system is designed so that security-sensitive values do not become permanently stored merely because an operation was logged.
This behavior is also covered by automated tests.
Import and Reconciliation
Legacy enterprise systems frequently exchange information with external systems through files, imports or periodic synchronization processes.
Imports create several risks:
- duplicate records
- partially processed batches
- inconsistent retries
- invalid relationships
- conflicting data
The modern architecture therefore treats import and reconciliation as defined workflows rather than arbitrary database writes.
Repeated processing must not silently create duplicated business state.
Validation and idempotency principles are applied where appropriate to make import behavior predictable and reproducible.
Messaging and External Communication
The original platform also interacted with external communication channels.
The modern domain model therefore includes communication capabilities such as:
- SMS
- Telegram
- internal messaging
These integrations are treated as external boundaries rather than allowing provider-specific behavior to spread throughout unrelated business logic.
This separation makes communication workflows easier to replace, test and reason about.
API-First Behavioral Contracts
The reference implementation exposes defined API behavior.
An API is not treated merely as a collection of routes.
Each operation has a contract describing aspects such as:
- request structure
- validation
- authentication
- authorization
- response structure
- failure behavior
- side effects
The project includes an OpenAPI specification so that important parts of the external interface can be described in a machine-readable format.
This reduces ambiguity between implementation and documentation.
Automated Testing
One of the largest differences between the 2014 system and the 2026 reference implementation is the role of automated verification.
Current reference status:
836 Automated Tests — PASS
Within those tests:
7,274 Assertions — PASS
These numbers represent two different concepts.
A test represents a scenario or unit of expected behavior.
An assertion verifies a specific condition within that scenario.
For example, a test may execute a payment operation.
Inside that scenario, multiple assertions may verify that:
- the response status is correct
- the payment exists
- the amount is correct
- exactly one ledger entry was created
- the invoice state changed correctly
- an audit event was generated
- unrelated records were not modified
The objective is therefore not to maximize the number of assertions.
The objective is to make important system behavior machine-verifiable.
What the Tests Actually Protect
The test suite covers behavior across different architectural layers.
Examples include verifying that:
- unauthorized users cannot execute protected operations
- users cannot access customer data outside their authorization scope
- issued invoices cannot be silently modified
- financial history cannot be destructively rewritten
- duplicate payment requests do not generate duplicate financial effects
- idempotency rules remain effective
- audit logs do not persist sensitive information
- repeated imports do not create unintended duplicates
- invalid state transitions are rejected
- database invariants remain valid
- failed operations do not produce unintended side effects
This is an important distinction.
The tests do not merely ask:
"Does this endpoint return HTTP 200?"
They ask:
"Did the complete business operation behave exactly as expected, including what must not happen?"
Negative behavior is part of the specification.
Side-Effect Control
One of the most important lessons in enterprise software is that a failed operation can be more dangerous than an operation that simply throws an error.
Imagine an operation that:
- creates a payment,
- updates an invoice,
- writes a ledger entry,
- sends an event,
- then fails.
If the transaction boundaries are incorrect, the application may report failure while leaving part of the operation committed.
The modern reference therefore tests not only successful outcomes but also failure behavior.
A rejected or failed operation must not leave the system in an unintended intermediate state.
This principle is critical for financial and operational consistency.
Database Architecture
The reference implementation uses PostgreSQL.
The database schema is not treated as passive storage.
It participates in protecting important invariants through:
- relational constraints
- foreign keys
- uniqueness rules
- appropriate data types
- explicit relationships
Application-level validation and database-level integrity complement each other.
Validation provides meaningful application behavior.
Database constraints provide a final structural boundary against invalid persistent state.
Golden Dataset
A defined Golden Dataset is part of the reference architecture.
The purpose of a golden dataset is to provide deterministic input data with known expected behavior.
Instead of validating complex system behavior against arbitrary development records, the project can execute scenarios against a controlled reference dataset.
This improves:
- reproducibility
- regression detection
- debugging
- behavioral comparison
If the same inputs unexpectedly produce different outputs after a change, the difference can be investigated systematically.
Golden Master Testing
The project also uses the concept of Golden Master Tests.
Golden master testing is particularly valuable when modernizing legacy systems.
A legacy system may contain behavior that is difficult to describe completely from documentation alone.
By capturing known expected outputs and comparing future execution against them, behavior can be protected during architectural changes.
This does not mean that every historical behavior must be preserved forever.
It means behavioral changes become deliberate.
A difference should exist because an engineering decision changed the specification — not because a refactoring silently altered behavior.
Architecture Package
The project contains an architecture package describing the system beyond implementation details.
The goal is to preserve engineering knowledge about:
- domain boundaries
- important invariants
- system responsibilities
- data relationships
- API behavior
- security expectations
- integration boundaries
Code explains how something currently works.
Architecture documentation should also explain why important boundaries and constraints exist.
That distinction becomes increasingly important as systems grow.
Security as System Behavior
Security is not treated as one middleware layer placed in front of the application.
It exists across multiple levels:
Authentication
Who is making the request?
Authorization
Is the actor allowed to perform this operation?
Object-level access
Is the actor allowed to access this specific resource?
Data integrity
Can invalid state be persisted?
Financial integrity
Can financial history be silently changed?
Audit safety
Can sensitive information leak into permanent logs?
Replay protection
Can retrying a request create duplicate effects?
These properties are part of the architecture and, where practical, part of the automated test suite.
Legacy Modernization Philosophy
One of the most important decisions in this project was not to equate modernization with rewriting.
A rewrite that ignores the accumulated business behavior of the previous system can easily produce cleaner code and a worse product.
Legacy systems often contain years of implicit knowledge:
- exceptions
- operational workflows
- business constraints
- edge cases
- historical assumptions
The modernization process therefore follows a different sequence:
Observe → Understand → Model → Specify → Implement → Verify
rather than:
Delete → Rewrite
This approach significantly reduces the risk of losing important business behavior during modernization.
2014 vs. 2026
The evolution of the project can be summarized clearly.
2014
The primary engineering objective was:
Build a compact operational system that solves the company's immediate business requirements.
The architecture optimized strongly for practical delivery and a small deployment footprint.
2026
The engineering objective became:
Build an explicit behavioral reference in which domain rules, security boundaries, financial invariants, APIs and expected side effects can be independently verified.
The difference is therefore much larger than:
Old PHP → Laravel 12
The real transformation is:
Implicit behavior → Explicit domain rules
Large procedural flows → Defined responsibilities
Database operations → Protected invariants
Manual confidence → Automated verification
Scattered authorization → Explicit RBAC
Mutable financial history → Immutable ledger principles
Retry uncertainty → Idempotent operations
Implementation knowledge → Executable specification
Key Metrics
| Metric | Value |
|---|---|
| Original implementation | 2014 |
| Re-engineered reference | 2026 |
| Original application size | ~23 MB |
| Legacy core PHP files | ~104 |
| Largest legacy files | Up to ~3,000 lines |
| Reference framework | Laravel 12 |
| Reference database | PostgreSQL |
| Automated tests | 836 PASS |
| Assertions | 7,274 PASS |
| API specification | OpenAPI |
| Architecture approach | Modular / Domain-Oriented |
Technical Highlights
- Legacy system reverse engineering
- Domain decomposition
- Modular monolith architecture
- Laravel 12 reference implementation
- PostgreSQL relational model
- Explicit service lifecycle
- Contract and billing domains
- Invoice and payment modeling
- Immutable financial ledger principles
- Idempotent operations
- Role-Based Access Control
- User-specific permission overrides
- Object-level authorization
- Audit logging
- Sensitive-data protection
- Import and reconciliation workflows
- SMS and Telegram integration boundaries
- API contracts
- OpenAPI specification
- Golden Dataset
- Golden Master Tests
- Automated regression testing
- Database invariant validation
- Side-effect verification
- Architecture documentation
Engineering Challenges
The difficult part of this modernization was not converting PHP syntax.
The difficult part was recovering the meaning behind twelve years of legacy behavior.
A line of old code may look unnecessary until the historical workflow that required it is understood.
A database field may look redundant until another process depending on it is discovered.
An unusual conditional branch may represent an important business exception rather than poor programming.
For this reason, modernization required separating three questions:
- What does the legacy system currently do?
- Why does that behavior exist?
- What should the modern specification define?
Only after answering those questions can behavior be safely preserved, corrected or intentionally removed.
That is why I consider this project an exercise in software engineering rather than simply framework migration.
What Changed in My Engineering Approach
The project also provides a useful comparison between two stages of my own development as a software engineer.
In 2014, success was primarily measured by whether the application solved the operational problem reliably enough to be used.
That remains necessary.
But it is no longer sufficient.
Today I also expect important behavior to be:
- explicit
- isolated
- testable
- reproducible
- documented
- observable
- secure
- maintainable
A system should not depend entirely on the original developer remembering why a particular condition exists.
Important knowledge should be represented in architecture, domain rules, contracts and tests.
That is perhaps the biggest difference between the two versions of this project.
Result
The result of the modernization is not simply a cleaner version of a 2014 PHP application.
It is a behavioral reference for a complex ISP/CRM domain.
The Laravel 12 implementation defines the expected behavior of the platform through:
- executable business rules
- relational constraints
- authorization policies
- API contracts
- deterministic reference data
- automated tests
- architectural documentation
With 836 passing automated tests and 7,274 passing assertions, critical behavior is no longer dependent solely on manual testing or undocumented assumptions.
The system's rules can be executed.
Its invariants can be challenged.
Its security boundaries can be tested.
Its financial behavior can be verified.
And architectural changes can be evaluated against a known behavioral baseline.
That is the fundamental difference between the project I built in 2014 and the system I re-engineered in 2026.
From Working Software to Verifiable Software
The original system proved that the business processes could be automated.
The modern reference goes further.
It defines what those processes mean, which transitions are valid, who may execute them, which side effects are permitted, and which invariants must always remain true.
2014 → 2026
From a compact legacy ISP/CRM application to an explicit, testable and verifiable software architecture.