Design system · three products15 min read
Diletta design system
Three products with different brands, built from one language, and a procedure for changing it
- 3
- products with different brands run on the same interface language.
- 263
- published versions. The furthest behind product is 24 back today; in July, 176.
- 1,022
- files of system updates landed in the app without changing a pixel.
- 3 days
- to ship a new product under its own brand. It used to take six weeks.
System in production. Product Designer at Diletta Solutions, from November 2021 to today.
00 File
| Product | The design system behind the company's apps |
| Products on it | Conta Bold, a digital bank, and CPF Seguro, which protects a person against fraud in their name · both live, and a third in progress |
| Role | Product Designer · Diletta Solutions |
| Period | November 2021 to today |
| Platform | Flutter, iOS and Android · with the color layer already published for web |
| Tools | Figma, Flutter, Git, Python |
| Scope | the whole system: the rules, the components, the automated check, the versioned delivery, and the link to Figma |
| State on 09 Sep 2026 | version 0.180.0, with 263 published versions |
One line. A new product joins the system by declaring a list of 55 colors. No component is rewritten, no test is changed, and a report points out what that product forgot to declare.
01 Summary
Problem. Three apps, three brands, one small team. The normal path is to copy the component library into each product. Six months later there are three similar libraries, and a fix made in one never reaches the others.
What I did. I separated language from brand. The brand became a list of colors the product declares. Components stopped knowing specific colors and started knowing roles: background, text, error, success. On top of that, I built three things that did not exist: an automated check that answers whether a component is done, a versioned delivery where each product chooses when to upgrade, and a written procedure for when a product needs something the system does not have.
Result. 263 published versions. The furthest behind product is 24 versions back today, and the other one is 8 back. In the July audit one of them was 176 versions behind, with not a line written about what had changed. One update of 1,022 files landed in a consuming app without changing a single pixel.
Role. Product Designer at Diletta Solutions. End-to-end design, the architecture of the system, and the ruler that judges what gets into it.
02 Context
The company sells software to other businesses and also runs its own products. Three apps are live or in progress, each with its own name, brand and audience. The design and front-end team is the same for all three.
That creates a problem that is simple to state and expensive to solve: how do you make three products look like three different companies without maintaining three libraries.
03 Constraint
A small team for three brands. There were not enough people to maintain three systems in parallel, and no way to stop the products to rebuild the base.
The app decides when it updates. The system arrives in each product as a versioned dependency. If bumping the version breaks a screen, nobody bumps it, and the system stops circulating. The consumer holds veto power, and that is healthy.
Real brands, not a light and dark theme. Each product needs its own identity, not a hue shift on the same blue.
Figma and code cannot drift. With two sources of truth, within two weeks nobody knows which one is right.
I own the system and I design inside one of the products that consume it. That is a permanent conflict of interest: the same designer who asks is the one who judges the request. Half the decisions below exist to move that judgment out of my memory and into a file.
04 The problem in numbers
Before, the question "is this component done?" had no answer. Done in the code is not done in Figma, and neither is done in the documentation the team reads.
The audit I ran in July measured the cost of that in one of the products: 176 versions of the system published since the last one that app had adopted, and not one line written about what changed in each.
Nobody updates a dependency without knowing what's inside. The system had stopped circulating, and no meeting had decided that: it was the consequence of there being no record.
Today the same number, measured on 08 Sep 2026, is 2 versions behind in one product and 18 in the other, with a changelog per version. That number is what says whether a design system is alive, and it is the easiest one to hide.
05 What existed before, and why it didn't work
The same problem showed up four years earlier, in DPS, and it was solved by hand: twelve palettes in light and dark, twenty-four screens drawn one by one in Figma. This case is the second attempt, at the root.
The common way to handle brand in a design system is the theme: the component knows the colors and you swap the values from outside, by configuration.
That works while the brands are similar. When they are not, drift begins: one product needs a button the others don't have, someone copies the component to change it, and the copy never comes back. That is exactly the path DPS, the company's earlier product, had walked while solving twelve brands by hand in Figma.
So the question changed. Instead of "how do I configure this component's color", it became "how do I make the component never need to know the color".
06 Decisions
D1 · The brand is a list of colors, and nothing else
The product declares 55 required colors and 7 optional ones. From those, the system derives the 59 roles components use, in both modes: background color, text color, error color, success color, elevated surface color, and so on.
No component knows a color. All of them know roles.
Measured consequence: the difference between one product and another is those 55 colors. Zero changes in components, zero in tests.
Why: while the color lives inside the component, every new brand is a change to the component. Once it lives outside, a new brand is filling in a form.
D2 · The tree has three tiers, and each one declares only what is its own
Two tiers are enough for three products: the language on top, the product at the bottom. The third appeared when one of the house's products became the base of a family: the white base and the first product's identity lived in the same place, so a new product was born wearing the first one's clothes. The middle tier's rule is one sentence: it declares mechanism, form and composition, and it never declares a value that only one product decides. Raw colour, a scale step, a gradient value, an image path, a font name.
What makes the rule enforceable is the ruler, and I wrote the ruler wrong first. A ruler that looks for a product's NAME cannot see identity that moved up as a value. A list of symbols carrying the brand's name closed at zero while thirteen text steps of that same brand sat inside the base, because a step is a number, not a name. The middle tier's ruler has had two columns ever since.
I gave up: a simpler drawing. Two tiers were easier to explain, and the third exists because the business asked for it, not because the architecture got prettier.
What I learned doing it: the tier had already been operating for three weeks without being written down, and I found that out reading my own record — a line from August was asking on behalf of a product the law did not know about, and a defect from that same week was one brand keeping another brand's artwork, because the key stored the file name and not the brand. A tier that is not written down makes every question arrive as a special case, and nobody counts special cases.
D3 · What the product forgets to declare, a report points out
If a product doesn't declare a color, it inherits the system's. That is convenient and dangerous: the screen ends up wrong and nobody notices until someone looks.
So there is a report that lists, for each product, what it declared and what it inherited without declaring.
Why: the model is only safe if the omission is visible. Without the report, the default value is a delayed bomb.
D4 · Extensibility through open pieces, not configuration hooks
When a product needs a component the system doesn't have, it builds that component from the system's pieces, which are public. The structure of a surface, for example, is split into top, content and base, and those three parts can be used separately.
Why: hidden configuration is a contract nobody reads. An open piece is a contract you can see.
What I gave up: control over each product's final result. In exchange, products stop copying the system to change it.
D5 · Done has six places, and the check looks at all six
A component only counts as done when it closes in six places: the written specification, the code, the catalog the team consults, the Figma file, the colors it resolves in each mode, and the way back, which is being able to read a product's own drawing and compare it against what the language dictates. The check runs from the command line and returns a number, not an opinion.
Consequence: in CPF Seguro, the 85 pieces under contract close all six sides, and 38 of them were compared against the actual drawing. The other 47 have a side that does not apply, each with its reason written down.
What I learned doing it: the first number I published was false. The program counting them silently skipped the cases it couldn't classify, so the total looked better than it was. An honest denominator is a prerequisite for any goal.
D6 · A product's request becomes a file with a written call, not a conversation
When a product needs something the language doesn't have, it writes a request in a fixed format. Every request goes into a register with four possible answers, the reasoning, and the version it shipped in. That register holds 196 lines today.
The rule it supports is this: a variant only moves up into the system on the second request. Two teams asking for the same thing is a sign the language is missing a word; one team asking is a sign that team's product is missing a piece.
Why: without a record of the first request, the second-request rule never fires, and the decision goes back to depending on someone's memory. A person's memory is not a procedure.
What I gave up: speed on each individual request. Writing the reasoning costs more than answering in a chat thread.
What it changed in me: one line in that register is a product proving I had announced a validation that did not exist, and the answer starts by admitting it. The register has to correct me too, otherwise it is theater.
D7 · The same change has to be adoptable on any platform
The company's products are not only apps: there is an operations panel and an internet banking surface, both web. So the decision is not "make a web version of the system", it is harder than that: a change to a system piece has to be adoptable in any product using it, regardless of platform, through the same adoption gesture that already exists today.
Measured state: the 93 color primitives already ship to web, and become 162 stylesheet variables. The layer components actually paint with, the 59 roles, is computed in Flutter and therefore does not yet exist outside it. That is the hole, and it is written into the decision instead of being discovered during implementation.
How I sized the step: I measured the operations panel before proposing a timeline. There are 3,746 component instances in the file, 2,689 of them visible, and the count is done by component identifier, never by layer name. Counting by name inflated the number by almost 30%.
D8 · The machine gathers and measures; the deciding is mine
The system has 36 command-line tools, and the whole check runs in five minutes. In August the question came up of splitting that across AI agents to go faster. I timed it before answering: the fourteen tools in the gate take 15.2 seconds. Splitting fifteen seconds buys nothing. The time was in the judgement, and judgement is serial, because every divergence I read changes the question the next one asks.
So the split is not by subject, it is by the right to write. One agent gathers facts and has no permission to conclude. One agent measures a single side and returns a number, and it is born with the defect built on purpose: if it does not go red in front of the defect, it does not ship. A third one is the advocate for the product about to receive the complaint, and it exists to kill the wrong complaint before it leaves the house. Publishing, bumping a version and adopting in the app remain a person's gesture.
I gave up: the speed I had been promised. The engine still takes five minutes, and I wrote that in the document instead of selling the opposite.
Consequence: out of 88 code deliveries in four days, 11 were fixing a measurement of mine that was wrong, 12.5%. None was caught by me re-reading them: every one was caught by a gate that builds the defect, or by a second voice asking.
07 Craft
loading the system's own pieces…
--cps-primary: #003BE0 data-theme: light tela: app
Where this does not hold yet: on the components tab the mode switches the frame and the previews stay light. That is a gap in the catalog, not in the language, and it is written here because I found it while recording this figure.
What exists today, measured on version 0.175.0:
- 128 pieces with a written contract, one per piece, and the contract travels inside the package instead of living only in the repository.
- 59 color roles across both modes, 355 icons, and 21 variant groups generated from the definition instead of drawn one by one.
- 516 entries in the single token list, which is the same color written in the three spellings the team uses: the code one, the open token format one, and Figma's.
- The four states every screen has to resolve, empty, error, failure and waiting, each with the right piece, plus the rules for artificial intelligence in the interface: mark, explain, verify and authorize, by level of risk.
- The 216 component names from the two products were compared one by one against the system's 104, and each one got an answer. None was left unanswered.
- There is a test product, built only to prove the model: an amber palette that depends on nothing but the system. If that product is cheap to create, the model works. It is the closing criterion.
- The fast check, for anyone touching a screen, runs in 16 seconds. The full one, in 2 minutes.
- Beyond that, there are tests that compare each screen's image, pixel by pixel, against the approved version. That is what allows the claim "not a pixel changed" without depending on anyone's memory.
08 Result
The update nobody saw. The system jumped thirteen versions at once, touching 1,022 files. In the consuming app: the 24 screenshots stayed identical, the 2,062 tests passed, and static analysis flagged nothing. Not a pixel changed.
That is the result of the system, not a side effect. A design system only circulates if updating is cheap and predictable. When it isn't, the product freezes on an old version and the system dies standing up.
Why these numbers and not others. Two things kill a design system, and both have names. First: nobody knows how much of the product actually uses the system, so adoption becomes a feeling. Second: nobody wrote down who decides what gets in, so the system drifts and turns into a pretty library each team works around its own way. That is exactly why the numbers here are distance in versions and lines in a register. I would rather say one product is 24 versions behind than say I have 128 components: the second number answers no question that the person paying the bill is asking.
Circulation. 263 published versions, and the worst lag today is 24 versions, against the 176 from the July audit. Every version has a changelog. The lag went from 18 to 24 since yesterday, and it is here because a number that only shows up when it improves is not measurement, it is advertising: the product that is behind has its own team and decides when it moves, and what it needs is a week of adoption, not a chart from me.
The check. In the measured product, the 85 pieces under contract close all six sides, and 38 of them were compared against the drawing, not merely declared.
The color ruler. Comparing one product's drawing against what the language dictates, 33 pairs match exactly, 29 diverge, and 22 of those divergences have a field-by-field reading: which color should be there, which one is. A divergence with an address is work; a divergence without one is an opinion.
A new brand. The entry cost of a new product is a list of 55 colors.
Who depends on it. It is not only my own work running through here. Three products consume the language, two of them live under different companies' brands, and the request register holds 196 lines with the product that asked, the call that was made, and the version it shipped in. It is the part of the work that keeps moving when I am not in the room, and that is why it lives in a file instead of in my head.
09 What I would do differently
I published a number before checking how it was counted. The program that measured "components done" silently ignored the cases it couldn't classify. The total looked good and wasn't. Today I write the test for the meter before I publish the measurement.
I trusted checks that measured presence, not the question. There is a file today with twenty numbered cases where a check went green over a real defect, or red over something that wasn't one. The class is always the same: presence is easy to measure and almost never the question. The first case is the clearest: the check looked at the color in the component tree when the question was the pixel, and blue at 18% opacity has a different final value than solid blue. If I went back, I would write the failing case before the check, for every one of them.
I wrote the changelog after I needed it, not before. The audit found 176 versions with no line of explanation. That wasn't pending paperwork, it was the reason the system had stopped reaching the products. Documenting what changes is part of shipping, not a task for later.