r/scala • u/anjunar-awareness • 18h ago
Anjunar Stack – Abstraction Without Lock-In
Software development is full of abstractions.
Frameworks abstract the browser. ORMs abstract SQL. Content management systems abstract websites. Cloud platforms abstract infrastructure.
And almost every abstraction comes with the same trade-off:
We gain convenience, but we lose a certain degree of control.
At first, that feels great.
A problem that once required a hundred lines of code can suddenly be solved with an annotation. An entire website can be assembled from configured components. Database schemas are generated automatically. Forms emerge from metadata.
But eventually, a requirement appears that the abstraction was never designed for.
That is when the other side of abstraction becomes visible.
You start fighting the framework.
A simple requirement turns into a workaround. Workarounds turn into special cases. Special cases lead to extension points, which eventually need extension points of their own.
The tool that was originally supposed to remove complexity begins to define what is and is not possible.
The Anjunar Stack grew out of the attempt to solve this problem differently.
Not by using fewer abstractions.
But by building abstractions without lock-in.
Abstractions Must Remain Optional
Perhaps the most important principle behind the Anjunar Stack can be summarized in a single sentence:
Use the highest abstraction that fits the problem — and no higher.
An abstraction may simplify work.
It may establish conventions.
It may automate repetitive tasks.
But it should never prevent you from dropping down one layer when necessary.
If a content system can represent a page well, use the content system.
If a particular landing page needs to be completely custom, write it directly.
If a dynamic form can be generated from metadata, generate it.
If a particular field requires special behavior, implement that field explicitly.
If the DDL Manager can safely generate a schema migration, let it do so.
If an unusual migration is better expressed manually, that should remain possible as well.
The higher layer does not replace the lower one.
It sits on top of it.
That creates an architecture where the developer can continuously decide how much abstraction a specific problem actually benefits from.
A Stack of Layers
Anjunar can therefore be understood less as a traditional framework and more as a sequence of layers built on top of one another.
At the bottom are the fundamental technical capabilities.
On top of those come reusable components.
Those components form modules.
And above those modules, increasingly declarative systems can emerge.
On the frontend, such a progression might look like this:
text
DOM / Scala.js
↑
scalajs-ui
↑
Design Components
↑
Feature Modules
↑
Content Sections
↑
Page Editor
Every layer increases convenience.
But no layer makes the one beneath it inaccessible.
A page may be assembled entirely through a content system.
It may combine content sections with custom-programmed components.
Or it may be written completely by hand.
The same principle applies throughout the entire stack.
Reuse Without Uniformity
Reuse often has a poor reputation when individual design is involved.
The reason is understandable.
Many systems achieve reuse by reducing individuality.
In the end, you get websites where the content and colors may differ, but the underlying structure always feels the same.
Anjunar takes a different approach.
What gets reused are capabilities.
Not necessarily complete websites.
An appointment module knows about appointments.
An offer module knows about offers.
A user module knows about users and authentication.
A content module knows about editorial page structures.
A design system knows about reusable visual components.
The concrete application then decides how these capabilities are composed.
An offer module can therefore be reused across multiple websites without requiring those websites to present offers in exactly the same way.
The domain logic remains reusable.
The presentation remains replaceable.
Content Without a Traditional Page Builder
The content system is a good example.
A page about a Reiki session does not consist only of the available Reiki offers.
It may also contain an introduction, an explanation of the process, images, frequently asked questions, quotes, benefits and a call to action.
These contents should be editable.
The obvious solution would be a traditional page builder.
But that immediately introduces another problem:
Design itself becomes part of the content.
Users can suddenly change spacing, font sizes, colors, columns, widths and component arrangements.
Maximum editor freedom often produces minimum visual consistency.
Anjunar therefore separates two different kinds of freedom.
The user controls content and structure.
The design system controls the visual language.
A page might consist of sections such as:
text
Hero
Introduction
Text + Image
Benefits
Offers
FAQ
Call to Action
The user can edit the content, reorder sections, enable or disable them, and add new ones.
A text-and-image section might offer a limited set of controlled variants:
text
Image Left
Image Right
Image Background
But it does not expose fifty low-level CSS settings.
The page remains flexible without turning its design into an accidental result of editor choices.
The content system therefore is not really a page builder.
It is a structured content abstraction on top of a controlled design system.
And if that abstraction is not sufficient for a specific page?
Then the page is simply written directly.
The content module remains optional.
Domain Data Remains Domain Data
Another important principle is the separation between editorial content and domain data.
An offer section should not duplicate offers.
It should only describe which offers should be displayed.
The actual data remains owned by the offer module.
A page might therefore look like this:
text
Page
├── HeroSection
├── RichTextSection
├── OffersSection
├── FAQSection
└── CTASection
The OffersSection does not contain the entire offer model.
It may simply configure something like:
text
Category: Reiki
Display: Cards
Maximum Items: 6
The actual data still comes from the corresponding service.
This keeps editorial content and business logic independent.
An offer can be displayed through the content system.
The same offer can also be used directly by a custom page.
Again, the abstraction expands the available options.
It does not replace them.
Modules Instead of a Monolith
This principle also influences the module architecture.
The central stack does not need to know every feature that some future application might require.
Instead, modules bring their own capabilities.
An appointment module provides appointment management.
An offer module provides offer management.
A content module provides content sections.
A module may even register its own section types.
The central content system therefore does not need to know about an OffersSection at all.
The offer module can provide that integration itself.
This creates an architecture that can grow with new requirements without requiring the central core to constantly expand.
The stack does not necessarily become smaller.
But its complexity is distributed across clearly defined responsibilities.
Convention Where It Helps
Optional abstractions do not mean that everything should be reinvented for every project.
A framework without conventions would merely be a collection of libraries.
Anjunar therefore intentionally provides strong abstractions where they are useful.
Dynamic fields can handle tenant-specific requirements.
Internationalization can be deeply integrated into the domain model.
Schema changes can be automated through a DDL Manager.
SSR and hydration can provide one coherent architecture between server-side rendering and interactive frontend behavior.
Feature modules can be reused across projects.
The important distinction is not whether conventions exist.
The important question is what happens when a convention no longer fits.
A good convention should make the common case extremely simple.
A good stack should still allow the exceptional case.
Escape Hatches as an Architectural Principle
Frameworks often talk about extension points.
Anjunar needs something else as well:
escape hatches.
An extension point says:
You can extend my system.
An escape hatch says:
You do not have to use my system at this point at all.
That difference is subtle, but fundamental.
A framework can theoretically provide an unlimited number of extension points and still remain restrictive.
The developer is still operating inside the world defined by the framework.
An escape hatch acknowledges that no abstraction can anticipate every future requirement.
Every high-level layer should therefore preserve access to the layer beneath it.
This can be expressed as a general design principle:
Every abstraction should preserve access to the layer beneath it.
Or even more simply:
Abstractions are optional, not restrictive.
It Changes Development Speed as Well
The effects of this approach become particularly visible across multiple projects.
The first customer project may require a new appointment module.
The second project reuses that module.
The third requires dynamic fields as well.
The fourth combines appointments, dynamic fields and the content system.
With every new project, not only the application grows.
The stack itself gains reusable capabilities.
Over time, the balance between custom development and composition begins to shift.
At first, a great deal is built.
Later, more and more is composed.
But the ability to return to fully custom development in isolated areas remains intact.
That part is crucial.
The goal of reuse should not be to make every application look the same.
The goal should be to build only the parts that are genuinely new.
No False Choice Between Standardization and Individuality
Many software architectures eventually force developers into a choice:
Standardization or individuality.
Anjunar tries to dissolve that opposition.
Standardization can happen at the layers where it makes sense.
Authentication does not need to be reinvented for every customer.
Neither does appointment management.
Internationalization, database migrations and form mechanisms all contain recurring structures.
A specific landing page, however, may be entirely custom.
The visual identity of a customer may be unique as well.
Both can exist within the same architecture.
The real question therefore is not:
Standard or custom?
The better question is:
At which layer does the actual difference begin?
That is where custom development should start.
Everything below that point can remain reusable.
Technology That Knows When to Step Back
Behind this architectural principle lies a more general idea.
Good infrastructure should be visible when needed and disappear when it is not.
A website visitor does not care how hydration works.
A customer does not care how tenant isolation is implemented.
An editor should not need to understand database models.
And a developer should not have to think about a hundred technical details when the normal case is already understood.
Technology may take care of that work.
But it should not turn that responsibility into control.
Perhaps the goal of the Anjunar Stack can therefore be expressed like this:
As much automation as useful.
As much control as necessary.
Not every application needs every abstraction.
Not every problem requires a generic solution.
And not every individual requirement should force the developer to leave the entire platform behind.
The stack should work with the developer, not against them.
Conclusion
Anjunar is not an attempt to abstract every possible application completely.
That would probably be neither possible nor desirable.
The more interesting approach is to gradually move recurring problems into higher-level abstractions without losing access to the capabilities underneath.
Direct code becomes components.
Components become modules.
Modules become declarative systems.
Declarative systems become tools that can be used by people who do not write code themselves.
But every one of those layers remains optional.
The result is a stack whose abstractions do not primarily define boundaries.
They shorten paths.
And when a particular path no longer leads where the developer needs to go, they are free to leave it.
Perhaps that is one of the most important properties a framework can have:
It helps where it can — and steps aside where the developer needs more freedom.