Structure preview

When a correct solution is poorly designed

A lesson on AI, random numbers and MOSAICO: from RandomNumero to RandomRange, distinguishing correctness, reuse and responsibility.

Articles /when-a-correct-solution-is-poorly-designed
When a correct solution is poorly designed

12 min

Salvatore Mosaico · 2026

Contents

Introduction

A program can consistently produce the correct result and still be poorly designed. This lesson stages a dialogue between a teacher, a student and artificial intelligence. At first the task seems simple: use JavaScript’s Math.random() to generate random integers. The real objective emerges at the end.

On the first day we build a general function returning an integer from 1 to N. On the second we extend it to an interval [a,b]. AI supplies a working formula. The teacher surprises the class: the mathematics is correct, but the solution violates an architectural decision in the project.

Four judgements must be distinguished: valid syntax, mathematical correctness, passing tests and architectural consistency. When a capability has already been isolated and tested at a lower level, a higher level should use it rather than reproduce its logic, if that is the system’s contract.

The right result is not enough. A good programmer also puts each responsibility in the right place. The following dialogues are teaching simulations, not transcripts of a real conversation.

1. Day one: from [0,1) to [1,N]

The teacher introduces Math.random(). It returns a pseudorandom number greater than or equal to zero and less than one: 0 ≤ Math.random() < 1. In [0,1), the square bracket includes zero and the parenthesis excludes one. Assume N is a positive integer, small enough for these classroom examples.

Classroom scene: understanding the interval

Teacher: We want an integer from 1 to N. What happens if we multiply the result by N?

Student: [0,1) becomes [0,N).

Teacher: In the mathematical model we can get decimals arbitrarily close to N, but never N.

Student: Math.floor gives us 0, 1, 2, …, N−1.

Teacher: How do we reach 1, 2, …, N?

Student: Just add 1.

function RandomNumero(N) {
    return Math.floor(Math.random() * N) + 1;
}

Multiply by N, round down, then add 1. With N=6 we simulate a die. In an ideal uniform model, each outcome corresponds to a subinterval of length 1/6. The actual implementation is pseudorandom and approximately uniform: this is not proof of perfect randomness.

2. Day two: an arbitrary interval

Student: How do we generate an integer between a and b, including both endpoints?

Teacher: How many integers does [a,b] contain?

Student: I would say b−a.

Teacher: Check [5,10]: 5, 6, 7, 8, 9, 10. Six values, but 10−5 is five.

Student: Then it is b−a+1, because both endpoints count.

For [5,10] we need six outcomes. RandomNumero(10−5+1) returns an integer from 1 to 6. To obtain 5, 6, 7, 8, 9, 10, shift every value four places: add a−1.

3. The artificial intelligence response

Asked directly in our simulation, AI proposes a familiar formula:

function RandomRange(a, b) {
    return Math.floor(Math.random() * (b - a + 1)) + a;
}

The mathematical explanation is correct: Math.random() produces a value in [0,1); multiplication by b−a+1 gives [0,b−a+1); Math.floor gives integers from 0 to b−a; adding a gives integers from a to b.

The endpoints also work: a random value of zero gives a, and a value sufficiently close to one gives b. Both formulas describe the same mathematical transformation. Separate calls need not return the same number, however: they make different draws.

The twist

AI: The requested function is Math.floor(Math.random() * (b−a+1)) + a.

Student: The formula is correct. We are done.

Teacher: No. Relative to our agreed design, there is a design error.

Student: But the results are correct. Where is the error?

Can there be an important defect even when a program returns the right values?

4. Where did AI go wrong?

Show the answer and explanation

The formula is not wrong. The problem is reimplementing random generation while ignoring RandomNumero(N), which was already built and tested. Math.floor(Math.random() * …) appears again: low-level knowledge is duplicated.

RandomRange becomes a second place that knows Math.random(). The version consistent with the project delegates to RandomNumero(b−a+1) and adds a−1 instead.

Teacher: Did AI solve the problem it received?

Student: Yes, the function returns a number between a and b.

Teacher: Did it respect the existing project?

Student: No. It solved again what RandomNumero could already do.

Teacher: Exactly: in this scene it addressed the local request without using the system’s history and architecture.

The defect is not revealed by running the function once, but by examining dependencies, maintenance and evolution. This is not an unavoidable inability of AI: providing context and an explicit reuse constraint can guide a different answer. Checking compliance remains necessary.

5. Why duplication becomes dangerous

Suppose we want to log every draw, replace the source during tests or adopt a generator suited to security requirements. We modify the base function:

function RandomNumero(N) {
    const result = Math.floor(Math.random() * N) + 1;
    console.log("Generated number:", result);
    return result;
}

The RandomRange version that uses RandomNumero automatically gets logging. The version calling Math.random() directly follows another path and logs nothing. The message records the intermediate value from 1 to N: logging the final result from a to b requires an explicit choice at the higher level.

Architectural consequences

  • Duplication: the same technical decision appears in several places.
  • Maintenance: changes must be remembered and repeated in every implementation.
  • Inconsistency: some functions may follow the new behaviour while others retain the old one.
  • Testability: replacing the random source centrally for repeatable tests becomes harder.
  • Responsibility: a higher layer knows a detail assigned to the base layer.

Duplication is not just repeated characters. It is a repeated decision. When that decision changes, every copy becomes a potential error. Reuse helps as long as the base function preserves its promised contract: an incompatible change still requires reviewing callers.

6. Reading the example through MOSAICO

MOSAICO builds software in layers. Base functions encapsulate elementary, tested capabilities; higher layers compose them without reproducing their internals.

  • Base level: RandomNumero(N) produces a random integer from 1 to N.
  • Higher level: RandomRange(a,b) computes the number of values in [a,b] and shifts the result.
  • Boundary: in this example only RandomNumero knows Math.random and Math.floor.
  • Benefit: internal changes compatible with the contract propagate to callers.

The direct formula crosses that boundary. The MOSAICO solution makes the hierarchy visible: RandomRange → RandomNumero → Math.random.

One question is: “What is the most direct formula for an integer between a and b?” The designer asks: “What capability does the system already have, and how can I compose it to obtain the new behaviour?” That second question turns a collection of functions into an architecture.

7. An important qualification

In a standalone program without RandomNumero, the direct formula is appropriate for the examples given. There is no universal ban on calling Math.random(). The error is contextual: we deliberately built a base layer and intended to reuse it.

Good design does not mean adding intermediate functions everywhere. It means assigning a responsibility clearly and avoiding duplication when it has already been centralised.

We can check that a and b are integers and a≤b:

function RandomRange(a, b) {
    if (!Number.isInteger(a) || !Number.isInteger(b)) {
        throw new Error("a and b must be integers");
    }
    if (a > b) {
        throw new Error("a must not exceed b");
    }
    return RandomNumero(b - a + 1) + a - 1;
}

The relationship between a and b belongs to the interval function. The base function must guarantee its own contract for N, including when called directly. This is a teaching version, not a general library for all Number values.

Technical note: limits and security

Number.isInteger does not guarantee a safe integer. A real library must define its supported domain, check the width b−a+1 and consider floating-point rounding. Even checking safe integers alone does not prove an exactly uniform distribution over enormous intervals. Our examples use small ranges such as [1,6], [5,10] and [−3,3].

Math.random is not cryptographically secure: these examples must not generate passwords, tokens or secrets. Centralising the generator makes replacement easier but does not automatically make the transformation of its output secure.

Conclusion: the programmer’s new competence

A local request can receive a locally correct answer. The overall system has a history, however: available functions, assigned responsibilities, permitted dependencies and decisions that must remain centralised.

The programmer must ask whether a solution belongs in the project. That requires architectural memory, understanding consequences and refusing to accept code automatically just because it works.

The difference is between “I can calculate the result directly” and “The system already knows the fundamental part: I should reuse it and add only what is missing.”

The defect was not in the returned number, but in ignoring knowledge already organised. The step from RandomNumero(N) to RandomRange(a,b) is small, yet captures MOSAICO well: reuse not just code, but responsibilities and decisions.

Technical references

Read also

The programmer in the age of artificial intelligence