Building a System That Measures Technical Debt by Behavior

Building a System That Measures Technical Debt by Behavior

Leader 2 3 11
calendar_today agoschedule15 min read

Technical debt is one of those phrases every software engineer knows.

Everyone talks about it.

Everyone complains about it.

Everyone has some of it.

And almost nobody measures it correctly.

We usually measure technical debt through things that are easy to count.

Number of TODO comments.

Number of outdated dependencies.

Code duplication.

Cyclomatic complexity.

Large files.

Old code.

Static-analysis warnings.

Test coverage.

These metrics are useful.

But they are not technical debt itself.

They are clues.

Technical debt becomes interesting when it starts changing the behavior of a system.

A service becomes slower.

Deployments become riskier.

Small changes require touching ten unrelated modules.

Developers become afraid of certain parts of the codebase.

Incidents become harder to diagnose.

A database migration that should take thirty minutes becomes a three-day operation.

A seemingly harmless feature causes regressions in completely unrelated functionality.

This is where technical debt becomes measurable.

Not as an abstract number attached to source code.

But as behavioral friction.

The idea behind this article is simple:

Instead of asking how messy the code looks, build a system that observes how the software behaves and estimates where technical debt is creating measurable friction.

That changes the problem completely.

We are no longer building another linter.

We are building something closer to a technical-debt observability system.


The Problem With Traditional Technical Debt Metrics

Imagine two services.

Service A contains 20,000 lines of ugly legacy code.

Service B contains 5,000 lines of beautiful modern code.

At first glance, Service B looks healthier.

But suppose Service A has:

  • 99.99% availability
  • 50 ms average latency
  • 15-minute deployments
  • very few production incidents
  • predictable change impact
  • easy debugging

Service B has:

  • frequent regressions
  • 400 ms latency
  • deployments that take hours
  • fragile integrations
  • high rollback frequency
  • mysterious production failures

Which system has more technical debt?

The answer cannot come from counting lines of code.

The smaller and cleaner-looking system may have more operational debt.

This exposes an important distinction:

Code quality is not the same thing as system health.

Technical debt is ultimately about the future cost of changing and operating software.

Therefore, we should observe what happens when the software changes.

That means measuring behavior.


Technical Debt Leaves Behavioral Footprints

Technical debt rarely announces itself.

It does not usually create a file called:

technical-debt.txt

Instead, it leaves traces.

Consider a developer modifying a payment service.

The first change takes one hour.

A few months later, a similar change takes four hours.

Eventually, every payment-related change requires touching:

PaymentService
OrderService
UserService
InvoiceService
NotificationService
DatabaseAdapter
LegacyPaymentAdapter

The codebase has accumulated coupling.

That coupling produces behavior.

The behavior might look like:

Change size: small
Files touched: 17
Tests affected: 84
Build time: 11 minutes
Deployment rollback probability: increasing

That is evidence.

Or consider a database.

A query starts at:

35 ms

Six months later:

280 ms

Then:

900 ms

Nothing necessarily looks catastrophic in a code review.

But the system is telling us something.

The architecture is accumulating friction.


What If We Could Observe Technical Debt Like Performance?

Modern systems already have observability platforms.

We measure:

  • CPU
  • memory
  • latency
  • throughput
  • error rates
  • traces
  • logs
  • database queries

Why not measure technical debt similarly?

Imagine opening a dashboard and seeing:

Technical Debt Health

Change Friction        ███████░░░  71
Coupling               ████████░░  82
Failure Amplification  ██████░░░░  63
Deployment Risk        █████░░░░░  54
Debugging Friction     ███████░░░  74
Performance Drag       ████░░░░░░  41
Test Fragility         ████████░░  86

These are not arbitrary numbers.

They could be derived from actual system behavior.

The goal is not to produce a magical "technical debt score."

The goal is to identify behavioral patterns that indicate increasing maintenance cost.


The Behavioral Model

We can model technical debt as a function of several measurable dimensions.

For example:

Debt =
    ChangeFriction
  + Coupling
  + FailureAmplification
  + DeploymentRisk
  + DebuggingCost
  + PerformanceDrag
  + TestFragility

But there is an important principle:

We should not treat these dimensions as universal truths.

Different systems have different architectures.

A batch-processing system behaves differently from a real-time API.

A game server behaves differently from an accounting platform.

A machine-learning pipeline behaves differently from an e-commerce application.

So our system should learn a baseline.

Instead of asking:

"Is 200 ms bad?"

we ask:

"Has this system become significantly worse relative to its own historical behavior?"

That is much more powerful.


Architecture of the Debt Measurement System

A basic architecture could look like this:

              ┌─────────────────────┐
              │   Developer Actions  │
              └──────────┬──────────┘
                         │
                         ▼
              ┌─────────────────────┐
              │   Source Control    │
              │      Events         │
              └──────────┬──────────┘
                         │
                         ▼
        ┌────────────────────────────────┐
        │      Behavioral Collectors     │
        ├────────────────────────────────┤
        │ Deployment Collector            │
        │ Incident Collector              │
        │ Test Collector                  │
        │ Performance Collector           │
        │ Dependency Collector            │
        │ Change Collector                │
        │ Runtime Collector               │
        └───────────────┬────────────────┘
                        │
                        ▼
              ┌─────────────────────┐
              │ Event Normalization │
              └──────────┬──────────┘
                         │
                         ▼
              ┌─────────────────────┐
              │ Behavioral Database │
              └──────────┬──────────┘
                         │
                         ▼
              ┌─────────────────────┐
              │ Debt Analyzer       │
              └──────────┬──────────┘
                         │
             ┌───────────┴───────────┐
             ▼                       ▼
      ┌─────────────┐        ┌──────────────┐
      │ Debt Scores │        │ Explanations │
      └─────────────┘        └──────────────┘

The important component is the behavioral collector.

We need to observe the system without requiring developers to manually report technical debt.


Event-Driven Debt Measurement

The foundation should be an event stream.

Every important engineering activity becomes an event.

For example:

{
  "type": "deployment",
  "service": "payments",
  "commit": "a92fd31",
  "duration": 842,
  "status": "success",
  "rollback": false,
  "timestamp": "2026-09-19T10:30:00Z"
}

Another event:

{
  "type": "change",
  "service": "payments",
  "commit": "a92fd31",
  "files_changed": 14,
  "lines_added": 340,
  "lines_removed": 120,
  "modules_touched": 6
}

And another:

{
  "type": "incident",
  "service": "payments",
  "severity": "high",
  "duration": 47,
  "root_commit": "a92fd31"
}

The system can correlate these events.

That is where the interesting information appears.


Measuring Change Friction

One of the strongest signals of technical debt is how difficult changes become.

Suppose a developer submits a pull request.

We can observe:

files_changed
modules_changed
dependencies_changed
review_cycles
build_duration
test_duration
merge_conflicts
rework_commits
rollback_events

We can combine these into a change-friction profile.

For example:

Change #1293

Files:             17
Modules:            8
Review cycles:      4
Rework commits:     6
Tests executed:   812
Test failures:      31
Build time:       13m
Deployment:        2
Rollback:          1

This tells us much more than:

Complexity = 14

The system can then ask:

Why does changing this component repeatedly require touching so much of the architecture?

That question is much closer to the actual meaning of technical debt.


Measuring Coupling Through Change Graphs

Static dependency graphs are useful.

But behavioral dependency graphs can be even more interesting.

Imagine that Service A rarely changes alone.

Whenever Service A changes, these services also change:

A → B
A → C
A → D
A → F

Over time, we can construct a change graph.

             ┌───────┐
             │ User  │
             └───┬───┘
                 │
        ┌────────┼────────┐
        ▼        ▼        ▼
     Payment   Orders   Profile
        │        │
        └────┬───┘
             ▼
         Database

But instead of merely representing technical dependencies, we measure co-change frequency.

If two modules independently change in 90% of the same pull requests, that is a behavioral coupling signal.

The code might not explicitly show a dependency.

The development process does.

That is fascinating.

The organization itself becomes an observability source for architecture.


Measuring Change Amplification

A small requirement should ideally produce a relatively small change.

Suppose a ticket says:

Add a field to customer profiles.

The actual change modifies:

27 files
11 database tables
6 API endpoints
4 frontend components
3 background workers
2 integration services

We can define:

Change Amplification =
    actual_change_surface
    /
    expected_change_surface

The exact formula is domain-dependent.

But the principle is powerful.

If a tiny conceptual change repeatedly produces a huge technical footprint, something is wrong.

Perhaps responsibilities are poorly separated.

Perhaps abstractions are leaking.

Perhaps data models are overly coupled.

Perhaps the architecture has accumulated historical assumptions.

The system doesn't need to know the exact reason immediately.

It can identify the behavioral anomaly.


Measuring Failure Amplification

Another powerful signal is how far failures travel.

Suppose one database timeout causes:

Database timeout
      ↓
Order service failure
      ↓
Payment retry storm
      ↓
Queue overload
      ↓
Notification delays
      ↓
API timeouts

The original failure was small.

Its impact was not.

That is failure amplification.

We can use distributed tracing to construct failure propagation graphs.

For each incident:

root cause
   ↓
affected service
   ↓
downstream services
   ↓
user-facing effects

Then measure:

Impact radius
Propagation depth
Propagation speed
Recovery time
Number of dependent components

A system with increasing failure amplification may be accumulating architectural debt.


Deployment Behavior Is a Gold Mine

Deployment systems produce enormous amounts of useful information.

Consider:

Deployment frequency
Deployment duration
Rollback frequency
Failed deployments
Hotfix frequency
Post-deployment incidents
Time to recovery

Now imagine a service where deployment time has evolved:

January     8 minutes
February   11 minutes
March      15 minutes
April      21 minutes
May        29 minutes
June       42 minutes

That is not just an infrastructure problem.

It may indicate increasing system complexity.

Maybe the test suite is growing without parallelization.

Maybe builds are becoming tightly coupled.

Maybe deployment requires coordinating too many services.

Maybe migrations have become dangerous.

The behavior is telling us that the cost of change is increasing.


Test Fragility

Test coverage is frequently used as a proxy for code quality.

But coverage alone can be misleading.

A better behavioral measurement is test stability.

Suppose a test suite contains 10,000 tests.

Every pull request produces:

Average tests failed: 18
Average flaky tests: 7
Average reruns: 3

Developers eventually stop trusting the suite.

That is technical debt.

A test that fails randomly has an operational cost.

We can measure:

Flake rate
Failure recurrence
Retry frequency
Test duration
Failure isolation time

Then identify test clusters.

For example:

Payment tests
    ↓
High flake rate
    ↓
Shared database fixture
    ↓
Parallel execution conflict

The system can surface this as:

Payment test suite has exhibited abnormal instability for the last 30 days.

That is much more actionable than:

Test coverage: 84%.


Measuring Debugging Friction

Some systems are not particularly slow.

They are simply difficult to understand.

A production incident occurs.

Engineers search logs.

Then traces.

Then dashboards.

Then source code.

Then database records.

Then finally discover the problem.

That process can be measured.

For each incident:

Time to first useful signal
Time to identify affected component
Time to identify probable cause
Time to mitigation
Time to root cause

We could call this:

Diagnostic Friction.

If the average time to identify a root cause increases, the system may have accumulated observability debt.

This is important because technical debt isn't limited to application code.

Sometimes the architecture works.

But nobody can understand why.


Measuring Performance Drag

Performance degradation is another behavioral signal.

But raw latency isn't enough.

We should track performance relative to change.

Suppose:

Commit A:
p95 latency = 120 ms

Commit B:
p95 latency = 130 ms

Commit C:
p95 latency = 150 ms

Commit D:
p95 latency = 210 ms

Now we can correlate performance with architectural changes.

Maybe a new abstraction introduced additional database calls.

Maybe a caching layer became ineffective.

Maybe a previously cheap operation became synchronous.

The system could detect:

Performance regression
        ↓
introduced by change
        ↓
persistent across deployments

That is a behavioral debt signal.


The Debt Timeline

One of the most useful features would be a timeline.

Imagine:

January
│
├── Service created
│
February
│
├── First coupling increase
│
March
│
├── Deployment time +18%
│
April
│
├── Test flakiness increases
│
May
│
├── Incident frequency increases
│
June
│
└── Change amplification crosses threshold

Now technical debt becomes historical.

You can see how the system got here.

That matters because technical debt is rarely created in one event.

It accumulates.


Debt Is Often Nonlinear

This is one of the most interesting aspects of the problem.

Technical debt does not necessarily increase linearly.

A system may behave perfectly for months.

Then suddenly everything becomes difficult.

Why?

Because architectural complexity often contains thresholds.

For example:

Complexity
    │
    │                /
    │              /
    │            /
    │          /
    │________/
    └────────────────── Time

At first, adding more features barely changes development cost.

Then a threshold is crossed.

Suddenly:

  • tests take longer
  • deployments slow down
  • changes touch more modules
  • incidents become harder to diagnose

The system has entered a different behavioral regime.

A good debt analyzer should therefore detect change points, not just averages.


Baselines Instead of Universal Rules

We should avoid simplistic rules like:

> 500 lines = bad
> 100 ms = bad
> 20 files = bad

Instead:

Current behavior
        ↓
Historical baseline
        ↓
Deviation
        ↓
Behavioral risk

Suppose an API normally processes requests in 80 ms.

A jump to 130 ms may matter.

For another API, 130 ms may be normal.

Technical debt measurement should therefore be contextual.

The system should learn:

What is normal here?

Then detect:

What is becoming abnormal?

Building the Behavioral Data Model

A simple event schema could look like:

CREATE TABLE engineering_events (
    id UUID PRIMARY KEY,
    event_type VARCHAR(50),
    service VARCHAR(100),
    commit_hash VARCHAR(100),
    timestamp TIMESTAMP,
    duration_ms INTEGER,
    metadata JSONB
);

Then we can store events such as:

change
build
test
deployment
rollback
incident
performance_regression
dependency_update
migration

The JSON metadata allows different event types to evolve independently.

Eventually, we can build specialized analytical tables.

For example:

change_metrics
deployment_metrics
incident_metrics
test_metrics
performance_metrics

The Debt Engine

The central analyzer receives historical events.

Conceptually:

class DebtAnalyzer:

    def analyze(self, service):
        changes = get_changes(service)
        deployments = get_deployments(service)
        incidents = get_incidents(service)
        tests = get_test_behavior(service)
        performance = get_performance(service)

        return {
            "change_friction":
                measure_change_friction(changes),

            "deployment_risk":
                measure_deployment_risk(deployments),

            "failure_amplification":
                measure_failure_amplification(incidents),

            "test_fragility":
                measure_test_fragility(tests),

            "performance_drag":
                measure_performance_drag(performance)
        }

But the interesting part isn't the code.

It is the interpretation.

The engine should not simply say:

Debt = 72

That number is nearly useless without explanation.

Instead:

Technical Debt Signal

Change friction increased 31% over 90 days.

Primary behavioral evidence:
- Average files changed per feature: 6 → 11
- Review cycles: 1.8 → 3.4
- Regression incidents: 2 → 7
- Deployment rollback rate: 1.2% → 4.8%

Most affected area:
Payment orchestration

Likely contributing pattern:
Increasing cross-module changes.

Now engineers have something they can investigate.


Explainability Matters

A technical-debt system should never become a black box.

If it says:

Debt increased.

the developer should immediately ask:

Why?

The answer should be traceable.

For example:

Debt increased because:

1. Change surface increased 42%.
2. Deployment failures increased 18%.
3. Test reruns increased 31%.
4. Incident recovery time increased 27%.

Every measurement should have evidence.

The system should be closer to an observability platform than an AI fortune teller.


Using Machine Learning Carefully

Machine learning could make the system much more interesting.

Instead of manually defining every threshold, we can train models on historical behavior.

For example:

Inputs:

files_changed
modules_changed
review_cycles
build_duration
test_failures
deployment_duration
rollback_rate
incident_frequency
latency
dependency_count

The model could estimate:

probability of high-friction change

Or:

expected maintenance cost

But there is a danger.

If the model learns from historical behavior, it may learn bad organizational habits.

For example:

If a team always creates huge pull requests, the model might decide that huge pull requests are normal.

Therefore:

Prediction is not the same thing as quality.

Machine learning should detect patterns.

Engineers should interpret them.


Technical Debt as a Control System

There is an even deeper way to think about this.

A software system is a dynamic system.

Developers introduce changes.

Those changes modify the system.

The modified system produces behavior.

That behavior creates feedback.

We can represent the loop:

        Developer Change
               ↓
        System Behavior
               ↓
        Measurements
               ↓
        Debt Signals
               ↓
        Engineering Decisions
               ↓
        Developer Change

This creates a feedback control loop.

The software is observing itself.

That is a much more interesting idea than a static technical-debt report.


From Dashboard to Automated Recommendations

Once we can detect behavioral debt, the system can generate engineering recommendations.

For example:

Observed:
Database migration failures increased.

Possible actions:
- split migration into backward-compatible stages
- reduce deployment coupling
- add migration verification
- move expensive operations outside deployment

Another example:

Observed:
Payment changes increasingly touch six services.

Possible architectural investigation:
- identify shared domain responsibilities
- inspect service boundaries
- evaluate event-driven integration

Notice the language.

The system should say:

investigate.

Not:

rewrite everything.

Technical debt analysis should help engineers think.

It should not pretend to architect the entire system automatically.


The Most Important Metric: Cost of Change

If I had to reduce the entire system to one principle, it would be this:

Technical debt becomes visible when the cost of change increases.

Everything else is evidence.

You can measure:

Time to implement
Time to review
Time to test
Time to deploy
Time to recover
Time to understand

Together, these create a picture of engineering friction.

A healthy architecture should make ordinary changes relatively predictable.

When the same class of change repeatedly becomes more expensive, the system is telling you something.


A Possible Technical Debt API

The platform itself could expose an API.

For example:

GET /api/v1/services/payments/debt

Response:

{
  "service": "payments",
  "period": "90d",
  "signals": {
    "change_friction": 0.72,
    "deployment_risk": 0.51,
    "test_fragility": 0.83,
    "failure_amplification": 0.61,
    "performance_drag": 0.42
  },
  "trends": {
    "change_friction": "+31%",
    "deployment_risk": "+12%",
    "test_fragility": "+24%"
  },
  "evidence": [
    {
      "signal": "change_friction",
      "reason": "Average modules touched increased from 3.2 to 6.7"
    }
  ]
}

Now technical debt itself becomes observable infrastructure.

Other developer tools could consume it.

IDEs could show warnings.

CI systems could comment on pull requests.

Dashboards could visualize trends.

Planning systems could identify areas where engineering effort is being consumed.


Pull Request Integration

Imagine opening a pull request and seeing:

Behavioral Impact

This change modifies the Payments subsystem.

Historical behavior:

Average payment PR:
- 5.2 files
- 2.1 review cycles
- 4.8 minutes CI
- 1.2% rollback rate

This PR:
- 14 files
- 5 review cycles predicted
- 11.3 minutes CI impact

Change amplification: HIGH

Investigate:
payment orchestration coupling

That is much more useful than:

ESLint: 0 errors

Both are valuable.

But they answer completely different questions.


The System Could Detect Architectural Decay

Architecture is not static.

A diagram might show beautifully separated services:

Users
Orders
Payments
Inventory
Notifications

Three years later, the actual behavior may look like:

Users ←→ Orders ←→ Payments
  ↑       ↓  ↘      ↓
  └──── Inventory ←─┘
       ↕
 Notifications

The architecture has drifted.

The source code may still claim that boundaries exist.

Behavior tells the truth.

That makes behavioral analysis particularly valuable for long-lived systems.


Technical Debt Has Different Species

Another useful feature would be classification.

Instead of one debt score:

Technical Debt

we could identify:

Architectural Debt
Testing Debt
Operational Debt
Observability Debt
Performance Debt
Dependency Debt
Data Debt
Deployment Debt
Security Debt
Documentation Debt

Each category produces different behavioral signals.

For example:

Deployment debt

longer deployments
more rollbacks
manual steps
higher failure rate

Testing debt

flaky tests
slow CI
low signal-to-noise
frequent retries

Architectural debt

high change amplification
high co-change frequency
large failure radius

Observability debt

long diagnosis time
missing traces
uncorrelated logs

This makes the output actionable.


What We Should Not Measure

There is a temptation to measure everything.

That is dangerous.

If we create 500 engineering metrics, developers will ignore all of them.

The system should focus on signals that correlate with actual engineering friction.

Good metrics should answer questions like:

Is changing this component becoming harder?

Is this service becoming more fragile?

Are failures spreading farther?

Are deployments becoming riskier?

Are engineers spending more time understanding the system?

Is performance degrading after architectural changes?

If a metric does not help answer a meaningful question, it probably does not belong in the core system.


The Future: Self-Observing Software

The most interesting possibility is not simply measuring technical debt.

It is building software that continuously observes its own architectural behavior.

Imagine a system that knows:

This component is changing frequently.

Its dependency surface is growing.

Its deployments are becoming slower.

Its failures are propagating farther.

Its tests are becoming less reliable.

Its performance is degrading.

The system can then recognize a pattern:

Architectural stress increasing.

That is powerful.

We have moved from static code analysis to behavioral architecture analysis.


Final Architecture

A mature version might look like:

                   ┌───────────────────┐
                   │ Source Control    │
                   └─────────┬─────────┘
                             │
                   ┌─────────▼─────────┐
                   │ CI/CD Systems     │
                   └─────────┬─────────┘
                             │
          ┌──────────────────┼──────────────────┐
          ▼                  ▼                  ▼
     Runtime Metrics      Traces             Logs
          │                  │                  │
          └──────────────────┼──────────────────┘
                             ▼
                  ┌─────────────────────┐
                  │ Event Normalization │
                  └──────────┬──────────┘
                             ▼
                  ┌─────────────────────┐
                  │ Behavioral Data    │
                  │ Warehouse           │
                  └──────────┬──────────┘
                             ▼
                  ┌─────────────────────┐
                  │ Debt Analyzer       │
                  ├─────────────────────┤
                  │ Change Analysis     │
                  │ Coupling Analysis   │
                  │ Failure Analysis    │
                  │ Test Analysis       │
                  │ Deploy Analysis     │
                  │ Performance Analysis│
                  └──────────┬──────────┘
                             ▼
                  ┌─────────────────────┐
                  │ Debt Explanation    │
                  └──────────┬──────────┘
                             ▼
       ┌─────────────────────┼─────────────────────┐
       ▼                     ▼                     ▼
   Dashboard              CI/CD                 IDE

This is not just a technical-debt calculator.

It is an engineering behavior observability platform.


The Bigger Idea

For years, software engineering has tried to measure technical debt by looking at the software itself.

Count the bad smells.

Count the warnings.

Count the duplicated code.

Count the complexity.

But software is not just source code.

Software is behavior.

And technical debt is fundamentally about how that behavior changes the economics of engineering.

If a small feature requires touching twenty files, that is behavior.

If a deployment takes two hours, that is behavior.

If a single database failure takes down five services, that is behavior.

If engineers repeatedly rewrite the same tests, that is behavior.

If nobody can diagnose an incident without searching through six systems, that is behavior.

Those behaviors are measurable.

And once they are measurable, they can become signals.

The most useful technical-debt system may therefore not ask:

"How bad is this code?"

It may ask:

"What is this system making engineers do?"

That question is much deeper.

Because technical debt is ultimately paid by people.

Developers pay it with extra hours.

Teams pay it with slower releases.

Companies pay it with operational risk.

Users pay it with bugs, downtime, latency, and missing features.

A codebase can look beautiful while quietly taxing everyone who touches it.

Another codebase can look ugly while being remarkably cheap to operate.

That is why behavioral measurement matters.

The future of technical-debt tooling should move beyond static analysis.

It should observe the entire engineering system.

Commits.

Pull requests.

Builds.

Tests.

Deployments.

Incidents.

Traces.

Performance.

Failures.

Recovery.

Change patterns.

And most importantly, the relationship between all of them.

Because the deepest signal of technical debt is not that the code became ugly.

It is that change became expensive.

And when a system can measure that expense continuously, technical debt stops being an argument in an architecture meeting.

It becomes an observable property of the system.

Something we can detect.

Something we can explain.

Something we can track over time.

And eventually, something we can manage deliberately.

That is the interesting future of technical debt:

not measuring how ugly the code is, but measuring how much friction the architecture creates.

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Merancang Backend Bisnis ISP: API Pelanggan, Paket Internet, Invoice, dan Tiket Support

Masbadar - Mar 13

The Zero-Net-Loss Fleet & The Mercenary Squad: A Live AI Economy

DEVPlank - Aug 4

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

Frameworks Are Institutional Memory

Ken W. Algerverified - Sep 17

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23
chevron_left
1k Points16 Badges
Kapiri Mposhi, Zambia.zambianmillenial.com
12Posts
4Comments
62Connections
Derek Mwale — Where Code Meets Creativity.

Related Jobs

View all jobs →