Programmable User Experience

I want to focus on levels of control, distance of control, effectiveness of control, reliability of a UI/UX.

On one side there are receipt for rigidity: framework like next.js, SSR.

On another side there is an old receipt for “configurability”.

I do not agree with none of both.

Let’s take as example the mobility means: train travels on rails, car travels on route.

So train is a rigid system of transport, with a rigid UI/UX: user need to go to the train station, user need to wait for the right hour, user need to select the right rails nr, users needs to be aboard in time, etc.

While car is a flexible system of transport, with an adaptable UI/UX: user can take the car whenever he want, user can put gasoline from wherever he want, user can travel wherever he want.

Here’s comes the Web

Still, even in Skynet era, most of the time a provider is serving a SaaS, while the user is just selecting “one-of-those”. This is very similar to what happens with the train: select where you want to go, then -> insert address in address bar (or select from bookmark), login into the system, do just a number of fixed operations.

Some software developer (the worst), comes from old OS, including Amiga OS, but also MS Windows 3.1, and Linux. I put all on the same family for this topic because what matter is the way they was used to evaluate a system: by configurability.

I am an old Windows user (from 1990), then a Linux user (from 1994), and also a Mac OSX user (from, say 2006). I know Amiga OS desktop just because a friend of mine shown it far time ago.

Last century, discussion goes this way: this OS is better because I can change it by just changing a parameter here and there.

Temptation is to provide a configurable UI to the user, so that “everybody will be satisfied”, but this assumption is a bias coming from long time ago.

During 1990’s you have to select just one OS. But you can select whichever SaaS.

“Stay with me, and return back everyday”. Read above, steps are:

  1. Insert address in address bar (or select from bookmark)
  2. Login into system
  3. Operate on SaaS

First comes First

  • Does user has simple way to insert your address?
  • Does user has your address in bookmark or preferred?
  • Does user use bookmark at all?
  • Does user has other mean to reach your service: email? browser ext? webapp? mobileapp?
  • Does user has the anxiety/urgency to reach your service?

Shortly: I wake up in the morning to reach the train, because otherwise I loose work, opportunities, money.

And not because I can not change the rails number, disposition of chairs, the sound in the ambient, and people around me.

It is a service that I need, and that bring me where I need to be, I just take it, and I know the path perfectly: I have bookmarked the path to the train station.

Anxiety driven UI/UX

Instead of flattering users, shake it up.

Not everything fit at the goal, but after all this rule is true most of the time:

  • Gaming: user returns because a championship, or for leaderboard, or for kudos
  • Social: user returns for “likes”, social approval, etc.
  • Business: user returns because he is running their business

User has the Power

To give power to the user, the GUI must be fixed, run on a fixed rail.

To compare with gaming: RPG vs Shooter vs Board games

While RPG are very interesting and stimulate fantasy, the game is played INSIDE the system, not outside.

Shooter and Board games does not require a lot of fantasy, are based on exact rules, and has an exact outcome: points.

On top of it: the easier are the game and its rule, the more the focus goes OUTSIDE the system.

The more the system is configurable, the more the user is disoriented and unfocused from the real staff outside.

A good UI must:

  • highlight the link between the GUI operation to the external outcome
  • explicitly show all available operations (not allow to configure the GUI so that the operation “magically appears”)

Software System is not DNA CRISPR

That is fun, but it is what some naive designer think about GUI: just because it is presentation staff, it is not required to be stupid staff.

So the approach is to think that from the backend you can change the frontend, and configure it.

But backend is far from frontend code, the most remote is the control, the less the control is deterministic.

That’s like to try to imitate nature in software development: nature is terribly complicated, it works thank to billion of years of evolution, try and fails.

Software development must be done in short time, not try and fails, not a monkey hitting keyboard fro millions of years.

Example: control Frontend from Backend

An Authorisation App:

  • Authorise third party integration by token
  • All third parties are in the same page
  • The list of third party type are provided by backend: WRONG
  • Each third party has some little difference, those difference are provided by backend: WRONG

I mean: the GUI must be on rails. What happens if those wrong choices are taken:

  • GUI code became “adaptible”
  • It is a kind of genetic code: it try to find solution between the number of variables

This is brainless: you already know the solution, and if you need to add a new platform as third partner, probably it will have its own idiosyncrasy, that requires to change the GUI.

A completely crazy choice.

Example: Inject Dependency on Injected Dependency on Injected Dependency …

This problem is more difficult to grasp: when and how to use DI.

The approach of using Inversion of control (the “I” in SOLID acronym) is good for dealing with testability of the code, and put under control the cyclomatic numbers.

But just focus on TDD and test definition might make loose of focus on a given project.

I love to currying functions, and to use those as a progressive DI. But this might lead to a complete loose of control.

First of all, in functional realm:

  • what to inject and in which order?
  • the injected function or class, might have injection?
  • what controls the injection of the injected? Must it be injected outside (before being used as injected) or inside (after injected)?
  • How many injection per currying?

This kind of problems lead to code hard to read, hard to understand, and hard to maintain, specifically maintaining the tests and tests case.

This choices can not be done upfront. And also this kind of problem arise also on OOP, defining the class hierarchy, even if hierarchy is avoided as much as possible.

What happens during development process is that specification is given progressively. And this happens more often than one would expect.

After 3 or 6 month of development, the code take a good shape for the specification that was given at that time. But if specification changes, everything start to become messy.

Who inject what, and where, and which dep need to be injected into the injected, et cetera, all are subject to changes, leading to duplication of code, repetition, grown of test and test cases.

I do not think LLM would solve it: the more the code base increase, the more the cost of sending context to LLM increase, the less effective become a coding agent.

Also LLM is not “reasoning” symbolically, it is reasoning “probabilistically and grammatically”: it just try a solution that “sounds good”.

Last Monday, while I was on my bike, a Ferrari surpassed me, I had time to ear how it sound: was that good?

Ferrari is good mechanical mean, able to perform very well, but is that something that “sound good”?

To blend LLM to act as coding agent is a good challenge, but not always a win. The code must stay there to keep solving problems, and it must be kept operative and clean.

I asked deepseek to generate a diagram of this mess:

But svg is not well rendered (I need help here). This is the mermaid source:

graph TD
%% Top-level consumer
A[Main App / Consumer]

%% First-level injections (currying / DI)
A -->|injects| B[Service A]
A -->|injects| C[Service B]
A -->|injects| D[Service C]

%% Second-level: dependencies of injected services
B -->|depends on| E[Repository X]
B -->|depends on| F[Logger]

C -->|depends on| G[Repository Y]
C -->|depends on| H[Config Manager]

D -->|depends on| I[API Client]
D -->|depends on| J[Cache Manager]

%% Third-level: dependencies of dependencies
E -->|needs| K[Database Connector]
F -->|needs| L[Log File Writer]

G -->|needs| M[DTO Mapper]
H -->|needs| N[Env Parser]

I -->|needs| O[HTTP Client]
I -->|needs| P[Retry Logic]

J -->|needs| Q[Redis Client]
Q -->|needs| R[Connection Pool]

%% Cross-injections & chaos
K -.->|also used by| I
L -.->|also used by| Q
P -.->|depends on| F

%% Currying complexity
subgraph Currying_Problems
S[Curried Function F1] -->|returns| T[Curried Function F2]
T -->|returns| U[Curried Function F3]
U -->|injects| B
T -->|injects| C
S -->|injects partial| D
end

%% Who controls injection?
subgraph Injection_Control_Issues
V[Who injects into E?] -->|Outside?| W[Caller before passing B]
V -->|Inside?| X[Service A injects into E]
Y[Order of currying?] -->|First arg? Last arg?| Z[Hard to trace]
end

%% Test duplication warning
subgraph Test_Growth
T1[Test: A + B + E + K]
T2[Test: A + B + E mock]
T3[Test: A + C + G + M]
T4[Test: D + I + O + P + F]
T5[Test: D + I + Q + R + L]
T1 -.->|duplication| T2
T3 -.->|repetition| T1
end

%% Style
classDef messy fill:#f9f,stroke:#333,stroke-width:2px
classDef unclear fill:#ffcccc,stroke:#900,stroke-width:2px
classDef test fill:#ccffcc,stroke:#090

class B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z messy
class V,W,X,Y,Z unclear
class T1,T2,T3,T4,T5 test

I try Markdown Renderer for GitHub

graph TD
    %% Top-level consumer
    A[Main App / Consumer]

    %% First-level injections (currying / DI)
    A -->|injects| B[Service A]
    A -->|injects| C[Service B]
    A -->|injects| D[Service C]

    %% Second-level: dependencies of injected services
    B -->|depends on| E[Repository X]
    B -->|depends on| F[Logger]

    C -->|depends on| G[Repository Y]
    C -->|depends on| H[Config Manager]

    D -->|depends on| I[API Client]
    D -->|depends on| J[Cache Manager]

    %% Third-level: dependencies of dependencies
    E -->|needs| K[Database Connector]
    F -->|needs| L[Log File Writer]

    G -->|needs| M[DTO Mapper]
    H -->|needs| N[Env Parser]

    I -->|needs| O[HTTP Client]
    I -->|needs| P[Retry Logic]

    J -->|needs| Q[Redis Client]
    Q -->|needs| R[Connection Pool]

    %% Cross-injections & chaos
    K -.->|also used by| I
    L -.->|also used by| Q
    P -.->|depends on| F

    %% Currying complexity
    subgraph Currying_Problems
        S[Curried Function F1] -->|returns| T[Curried Function F2]
        T -->|returns| U[Curried Function F3]
        U -->|injects| B
        T -->|injects| C
        S -->|injects partial| D
    end

    %% Who controls injection?
    subgraph Injection_Control_Issues
        V[Who injects into E?] -->|Outside?| W[Caller before passing B]
        V -->|Inside?| X[Service A injects into E]
        Y[Order of currying?] -->|First arg? Last arg?| Z[Hard to trace]
    end

    %% Test duplication warning
    subgraph Test_Growth
        T1[Test: A + B + E + K]
        T2[Test: A + B + E mock]
        T3[Test: A + C + G + M]
        T4[Test: D + I + O + P + F]
        T5[Test: D + I + Q + R + L]
        T1 -.->|duplication| T2
        T3 -.->|repetition| T1
    end

    %% Style
    classDef messy fill:#f9f,stroke:#333,stroke-width:2px
    classDef unclear fill:#ffcccc,stroke:#900,stroke-width:2px
    classDef test fill:#ccffcc,stroke:#090

    class B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z messy
    class V,W,X,Y,Z unclear
    class T1,T2,T3,T4,T5 test

How much solid is that “I” in the SOLID acronym?

Just another case where sounds good does not means it would work.