78

I'm reading about software architectures like Hexagonal architecture, Onion architecture etc.

They put a big emphasis on decoupling. The business logic sits at the centre and the UI sits on the outside. The idea is that the UI should not touch the business rules at all. It should be totally dumb and should just relay commands and display any updated output.

The problem I have is that I find this quite difficult to imagine in practice. The UI will likely implement conditional rendering, and much of that conditional rendering is a business rule in itself.

Imagine a shopping cart system. For whatever reason the client decides they do not want promo codes to be added to an empty basket (this maybe isn't a great example but run with me). In your conditional render in the UI you would have to check if items.Count == 0 - and boom, you've just implemented business logic rules in the UI.

Or would you have this rule in your DTO with a property called CanUserInputPromoCode? Even then, the DTO isn't part of the domain logic, is it?

Update: this is getting quite a bit of attention. A better use case regarding the promo code would be that users cannot enter any promo codes unless the value of the basket exceeds $50. That's a bit more clear rather than this being solely a UI issue.

MSOACC
  • 965

11 Answers11

56

When different people talk about decoupling the UI from the business logic, they sometimes mean different things:

  • They can mean not to implement any UI independent logic inside an UI layer - all logic which can be useful outside an UI should be placed somewhere else.

    Your example shows such a case. CanUserInputPromoCode may be useful out of the UI, or at least not restricted to a specific UI design. The most natural place for CanUserInputPromoCode is probably not a DTO, but a business object Basket. That will allow to reuse it in case the Basket object might get used inside a non-UI process (for example, in an automated test).

  • Or they mean to decouple the system from the specific UI technology. This can be realized by introducing architectures like MVC, or MVP, or MVVM, where there is an extra view model layer or presenter layer, which contains the UI controlling logic, but communicates with the UI through an interface (and so keeps the UI technology exchangeable).

    Note MVC, MVP or MVVM are not necessarily used for a UI technology exchange. For example in Web applications, designers often want or need the UI controlling logic on the server, whilst the UI uses HTML or Javascript on the client. Or they want UI controlling logic to become subject of an automated test.

And yes, UI controlling logic is also "business logic". Your shopping cart example may require a clearly visible "Pay Now" button of a certain size and color at the check out, before any financial transaction takes place. That kind of business logic cannot be decoupled from the UI itself, but that is not meant in Hexagonal architecture or Onion architecture when they speak about decoupling from UI.

Doc Brown
  • 218,378
44

You need to be clear about what is meant by business logic.

Business logic refers to the logic that act on the data.

It is not logic that validates the data.

It is not logic that reacts to the data.

It is not logic that displays the data.

Showing a list differently if count is 0 is not business logic; it is presentation logic. Ideally, it should not be implemented where you process the data to display, it should be implemented by the list component which does not even care what your data looks like.

While deciding how to display a list has zero business logic, there are some forms of conditional rendering that touches business logic. One example is what features a user can access depending on his role. In this case, given the data that is the user's role the system must restrict access to features.

Restricting access is an act. Since processing the user's role is business logic restricting access to features must not be implemented in the UI. It needs to be implemented in the core of the application (for a web or client/server app this is the back-end). The functions/methods themselves should throw an error if a user is not allowed to use them.

The UI can show/hide navigation items for features allowable to the user, but this should not be where the decision is made. In this case, showing or hiding what the user is allowed to do is presentation logic. It can be implemented purely in UI based on the user's role, in which case you're re-implementing business logic as presentation logic and, in my opinion, it's OK as long as it's not the only way you're restricting user's access to features. A more modular approach is to have business logic in the back-end return what the UI is supposed to render as navigation items. But that's not necessary in my opinion.

Another example is the updated question of not allowing the use of promo code if there is not item submitted with the order. Note that the real business rule is:

Promo code cannot be used with empty order

or

Promo code cannot be used if order is below $50

As such this business logic must be implemented in the back-end and the back-end must return an error if someone attempts to use a promo code with an empty order regardless if the order is submitted by the web UI, by an app, by a third-party app or by API access. As you can see, it is impossible to implement this business rule in the UI. It must be separated from the UI and implemented outside the UI.

Now, to improve the user experience you may also hide the promo code field if the shopping list is empty or below a certain value. As I have mentioned above, I don't have a strong opinion if this is implemented in the UI itself (basically reimplementing business logic as viewmodel logic) or have the business logic expose an API to tell the UI what to display. This is the same decision making process as the role/features example above.

While there are many ways to interpret what is meant by "business logic" I've found treating "business" as what your application does to data to be the most sensible. What data does to your application is not business logic. It is other kinds of logic — usually either validation or presentation.

Even a GUI-heavy application can separate business logic from UI logic. One good example is Photoshop. You may think that Photoshop must mix what you do to images with the UI but you are wrong. The UI is just one way to use Photoshop to perform actions like applying airbrush stroke here or selecting color there to images. However, you can do exactly the same thing in at least two other ways: inside plugins and with javascript scripting. All operations in Photoshop are not tied to the UI. The UI merely has access to the operations but you can also access the operations outside the UI.

slebetman
  • 1,463
  • 9
  • 9
13

You can't separate a UI from logic, but you can separate most to all of the "business" logic. Conditional rendering is fine, and you don't really need to involve business logic to do it. Rather than checking item count in your example, you could reference a "promoVisible" boolean. The DTO exists to transfer data, part of the data that needs to be transferred in most apps are the results of decisions like should a promo code get displayed. The real difficult part is separating validation logic from a UI, though even then the best practice is to validate separately on the back end since the UI can't be fully trusted.

Ryathal
  • 13,486
  • 1
  • 36
  • 48
6

Yes, it’s possible. The light bulb went off for me when I realized that “business rule” is not synonymous with “if statement”. You can have conditional rendering without specifying why the element is rendered (or not).

The shopping cart can have zero or more items and the promo code input can be included or not. From the UI’s perspective these are two independent concerns. As you said, you may pass a DTO to your UI that includes a list of shopping cart items and a flag for showing or hiding the promo code.

The business rule is that the state of the flag depends on the number of items in the cart. The UI doesn’t know about that business rule. It looks at the flag and it looks at the list and it’s oblivious to the fact that these two properties are connected.

2

Let’s say your ui should show or hide button X according to business logic. Simple: The business logic tells the ui from the outside whether to show the button. More precise, whether the user should be able to do an action Y that would be caused by pressing the button or not, it is up to the ui how this is accomplished.

Now the Ui doesn’t need to know about the business logic, and the business logic doesn’t know about the button.

Elsewhere this is known as model-view-controller pattern. The model contains the data. The view displays the ui. The controller uses business logic and data to control the view, and modifies the data according to actions in the view.

gnasher729
  • 49,096
1

You're taking a much too literal reading on what constitutes "business logic", and by extension you're not mentally separating your domain objects from your viewmodels/DTOs either.

Frontend vs backend validation?

Imagine a shopping cart system. For whatever reason the client decides they do not want promo codes to be added to an empty basket (this maybe isn't a great example but run with me). In your conditional render in the UI you would have to check if items.Count == 0 - and boom, you've just implemented business logic rules in the UI.

If we forget having a nicer user experiences for a moment, that validation wouldn't even be in the UI.

Your frontend would allow the user to enter a promo code, the UI sends a request to the backend, and the backend will throw an error. That is the baseline behavior you want, because it is the most secure. If you were doing those checks only on the frontend, the customer could find a way to circumvent them since they can mess around in their own browser. But they can't mess around with your backend, making it the ideal line of defense against enforced validation.

It's secure, but it's not very user friendly. The user spent time and effort filling out your form, only to then be met with an error that they could've been informed of minutes earlier when they were still filling out your form.

This nicer user experience is achieved by having the frontend perform additional validation (of the same rule), which it can do in real-time so it can alert the user immediately.
If the user somehow decides to circumvent that frontend validation, then they can probably find a way to send that (invalid) request to the backend anyway, but the backend will still perform its own checks and reject the invalid request anyway.

If you're allergic to writing the same validation twice, then only do backend validation and accept the decrease in user experience.

"Business logic"

Business logic is a very abstract term. Commonly, it refers to what the backend does, not the frontend. However, for a sufficiently complex frontend app, you might have a frontend dev team who refer to the JS/TS scripts of the frontend app as business logic (as opposed to the views of the frontend app).

You're clearly in the first camp here, specifically referring to the business layer of the backend service. In this context, business logic happens on business objects.

Business logic vs view logic?

Your frontend should never receive business objects, since you're doing proper layer separation (onion, hexagonal, ...). Your frontend is working with viewmodels or DTOs, which are not business objects.
By that definition, any logic that operates on these viewmodels or DTOs is not business logic, it is view logic.

As we established before, what a backend dev might refer to as "view logic", a frontend dev could refer to it as "business logic", because the frontend is that developer's business. This is a matter of subjective scope based on who you ask.

Based on your question, it seems that you mentally haven't quite separated your layers the way your code has already separated them. You're still thinking of your viewmodels/DTOs as business objects, which they aren't.

You cannot reasonably expect to have the frontend do literally no logic. Otherwise, what would be the point of JS/TS or any frontend framework?

Flater
  • 58,824
1

I think that this discussion is stumbling over what is meant by the term, "business logic."

The analogy that I like is this: "You can discuss 'business logic' with a businessman, making no reference at all to a digital computer." You can implement "business logic" using paper and filing-cards, like they used to do a hundred years ago. Computer software at various levels implements that "business logic," but the users who are using the system to perform their jobs are following that "business logic" also, in their own human activities.

1

Your confusion arises from the poor state of language and definitions in the industry. Here, I will break it down for you, then apply to your example.

What is business logic

To answer your question, the language and ideas need to be clarified first. Business Logic is actually quite abstract and broad. It's clearer to reason about "System" Logic, which itself can be broken down to concrete ideas:

  • Authoritative Process Logic - changing state of the data (let's simply imagine a single database), and enforcing rules (eg. validation). This is minimally required, you cannot only rely on the UI.
  • Authoritative Data Access Limits - who can access what.
  • UI Guiding Logic - catch user mistakes as they happen. (eg. UI validation)
  • UI Access Logic - The UI might hide a section of the UI depending on the user's role.
  • UI Behavior Logic - guiding the user through data. Animations. Wizards, and Steps.
  • UI Human Formatting - presenting data in a way that's ideal for humans to work with. (Thanks @gnasher729)

A backend/server is an authoritative agent that can enforce rules, because there the code is isolated from modification from the user. If you only rely on the UI, then it's possible to use the API in an invalid way. The backend IS the system. The UI is the remote control for the user. If you can duplicate logic in the UI, then it becomes more efficient for the user - a smart remote control.

Applied to your example

The problem I have is that I find this quite difficult to imagine in practice. The UI will likely implement conditional rendering, and much of that conditional rendering is a business rule in itself.

This is simply UI Access Logic. The UI cannot enforce that, only a backend (which is an authoritative agent) can authoritatively enforce access rules.

Imagine a shopping cart system. For whatever reason the [shop] decides they do not want promo codes to be added to an empty basket (this maybe isn't a great example but run with me). In your conditional render in the UI you would have to check if items.Count == 0 - and boom, you've just implemented business logic rules in the UI.

That's UI Guiding Logic.

Or would you have this rule in your DTO with a property called CanUserInputPromoCode? Even then, the DTO isn't part of the domain logic, is it?

Let's just say "Backend". This is Authoritative Process Logic. From an enforcement perspective, it doesn't matter how you structure this in the backend, as long as it's in the backend somewhere, secured and only modifyable by the shop vendor.

Update: this is getting quite a bit of attention. A better use case regarding the promo code would be that users cannot enter any promo codes unless the value of the basket exceeds $50. That's a bit more clear rather than this being solely a UI issue.

The answers above remain the same.

In conclusion: the information about "architectures" is not helping unless you understand the elementary principles of how system logic is used in enforcing business rules. The rest is lower level design decision-making.

Kind Contributor
  • 890
  • 4
  • 13
0

You can't fully separate UI and business logic.

In many cases, your BL exists mostly to support the UI. Making BL really UI agnostic would mean implementing a lot of code paths that are never taken. Dead code is bad.

Conversely, a BL unaware of a specific UI needs will cause reimplementation of whatever behaviors fails to be exposed. Duplicate code is bad. Inappropriate intimacy is worse.

Decoupling is about making sure the interface between BL and UI is sane and designed in a way that allows change and testing of one without the other (ideally), or (practically) with limited awareness of the other. Because separation of concerns reduces complexity, and complexity confuses people, leading to bugs.

Typically:

  • BL doesn't depend on any UI code, libraries or shared state
  • Correctness is always implemented in the BL layer
  • Business behaviors required by the UI is exposed as an API by the BL layer

In your example, if the rule is: "promo codes cannot be added to an empty cart", and you want your UI to hide that element if the cart is empty, you would:

  • Have a BL API like canEnterPromoCode(), implemented with cart.isEmpty(), with tests and the works
  • Have the UI render the promo code input based on canEnterPromoCode()

...because the UI doesn't need to know under which conditions promo codes are allowable, just whether to provide an input or not. And other options have their own issues the moment you want something like BL.addPromoCode(…): you'd want that method to fail if the cart is empty, but you can't do that without a BL->UI call or code duplication.

ptyx
  • 5,871
0

Update [to example scenario]: [The] users cannot enter any promo codes unless the value of the basket exceeds $50.

Your example case certainly qualifies as business logic (latest since the clarified example), that itself however does not necessarily imply your UIs conditional code being relevant to the business process.

Security

Your code (items.Count == 0) itself is being transferred to the client and thereafter executed on the client's machine, i.e. in an entirely uncontrolled environment. Regardless of how well-written1, unit-, integration- and e2e-tested it may be, it's in the nature of the runtime env that it can be tampered with at will.

This is why it cannot make important decisions. Ever.
It's not the Javascript, it's not the engineers, it's the execution environment.

The decisive evaluation of the condition for promotions being fulfilled must happen server-side during the checkout step of the sales process.

Decoupling

The attempt to literally absolutely "decouple" a user interface from a server-side process, both of which are meant to model the same real-world process (a sale), has - in my limited experience - always consumed vast amounts of resources that could have been allocated much more sensibly. I was once witness to a sad, slow, painful death in the vastness of the desert of layers of abstraction, useless indirection, and false generality. It wasn't fun to be a part of at all.

Deduplication

Security aside, shipping your code to production as proposed will lead to a regression in overall maintainability of the software.

In your example, the predicate that must be true for a promo code to be valid is net total sale exceeding the threshold of $50. Easy enough a condition to check.

  • What if management decides to change the threshold value?
  • What if the predicate is being repeatedly extended over time by further conditions?
    (the customer must have been registered for period n, the annual total spend of the customer must exceed m, e.g.)
  • What if the threshold is specified to become a derived value of sorts?
    (say, $50 on a rainy day, $10 when it's sunny out and $200 the week before the holiday's)

Or would you have this rule in your DTO with a property called CanUserInputPromoCode? Even then, the DTO isn't part of the domain logic, is it?

No. That wouldn't be much better. (Though better.)

The predicate condition(s) in the UI code should be statically generated during build-time from the backend source code directly or from a shared, version-controlled configuration file.

Iff requirements ever become so dynamic that the predicate must be variable at runtime, then the client application should fetch the latest condition(s) as needed from a dedicated server-side endpoint (or from a dedicated server-side query resolver, for the cool kids in the GraphQL camp).

A fictitious, but realistic scenario

In terms of the layering of the codebase and day-to-day interdependence between the work of UI engineers, backend engineers and your DevOps team, go ahead, decouple away.

Generally, I am rather flexible on the topic of language / stack choice, too.
However, some language(-family) that supports Generics (Java, C#, Typescript), ideally static, compile-time polymorphism (Rust, C++) for the server-side is invaluable.

Again, deduplicate, automate, abstract.

I want a significant part of my runtime code to be generated. I don't want to see macros or a templating language (moustache, handlebars, ejs, etc.) in my codebase either though. Generated code should be compiled, type-checked, tested - it deserves all the usual engineering goodness the rest of your codebase (hopefully) enjoys.

Say some team member implements a new kind of HTTP response never used in the software so far. They add HttpStatusImATeaPot = 418 to enum class HttpStatusCode {} in the backend's HTTP utility library and use it in an endpoint.
During build, the same key/value should be automatically added to a generated Typescript enum of the same name. At this point, the UI's development build should issue a warning on an exhaustive check that an enum value is missing from a switch statement in the client http response handler. CI builds should fail with an error. Continuous Deployment should exclude the merge until the UI has caught up.

In case of compilation misconfig (e.g.) and a build having succeeded that shouldn't have, an end-2-end test before deployment should verify the HTTP responses of the runtime code.

This isn't a one-off requirement either. I want this to be available to the UI team for every single <InsertFancyBusinessProcessName>(Rejection|Success)Reason. For every subclassed error or exception that is intended for the client.

--> How are you going to "decouple" that?
--> The entire premise is faulty, I believe.

N.B. I am not advocating for any significant amount of runtime symbols /symbol names to be shared. Literal values and types though? yes please!

Conclusion

Integration and end-to-end tests, for instance, are not (superfluous) tight coupling. On the contrary, those are a pre-requisite for the long-term success of a project.

Having a common foundation (shared static values, depending on languages involved shared library code) between layers is not tight coupling at all.
That's simply sane architecture.

If UI and Backend are so professionally "decoupled" indeed, that UI engineers are manually duplicating semantically meaningful literals, which already exists on the other end of an API, then something's gone in an entirely wrong direction.
If they are "so totally decoupled" from the API that they must maintain their own adapter code for it, whose benefiting, exactly?

1 Sidenote: Use strict equality checking in ES.

0

The UI is business logic already, if you think about it. In fact it is as crucially part of the domain as the rest of it, at least in principle. The UI structure should and normally does reflect the overall structure of the system. Elements and structures in the UX, both spatial as well as temporal, both explicitly defined as well as implicitly observable, already inherently reflect those in the whole system, even if not in a reified/codified way. Additinally, design elements and themes are often driven by or reflective of domain requirements (think shopping cards and global holidays, special sales, etc).

All in all: the UI always evolves alongside the “pure” domain logic, and domain logic must live up to and evolve according to newly discovered or changing realities at the UI level.

So why shouldn’t we view the UI as a core part of the domain? Of course we shouldn’t normally leak HTML and CSS into our “pure domain” logic, but UI rules, navigation structures, events, not to mention dynamic contents — all are already unavoidably “coupled” to the domain, even if not in obvious ways by today’s standards.

A lot of modern languages can be transpiled into Javascript nowadays, the browser can store lots of data, even for offline processing. We have websockets and peer-to-peer data solutions (e.g. GunDB). Node.js can render stuff on the server side with headless rendering. Expected screen layout can be known prior to the HTTP response being sent, feedback from the layout could be incorporated back into some domain logic processing once or more —i.e. domain logic could use headlessly rendered subparts of the final UI output as a sort of constraint engine or reality check before finalizing the results. For example for finding the optimal way in real time to present product search results based on whatever domain criteria currently active — no need to sync with the HTML/CSS team. Entire changesets of domain and/or UI logic could be played out with automatic analysis of outcomes, if the UI was modeled as an integral part of the domain from the grassroots. We’re seeing a melting together of the server and the client, for those that want to leverage the possibilities.

Why not design our domain code to already incorporate the user interaction into the big picture from the grass roots, and then simply add a UI layer to hold only the topmost final rendering bits. So there would be an ubiquitous model that covers all aspects of the system, a registry to specify how to render which combinations of user-facing state, and a single controller, or engine, to make the UI alive, to stitch everything together dynamically (and generate any client side code if necessary), as opposed to multiple/many ad-hoc controllers.

Think in the lines of Direct Manipulation Interfaces, but even navigation and layout could be seen as Directly Manipulable aspects of (transient or persistent) domain state. Check out the ‘surrogate objects’ concept in https://engineering.purdue.edu/~elm/projects/surrogate/surrogate.pdf.

As you add concepts and relationships to your domain, the engine tries to glue everything together, finds the holes introduced by these newly added denizens of the domain landscape, and informs the team which new state combinations need additional rendering elements (widgets?) in order for the UI to be complete. You implement those, and the UI “gets synced” for free. Why would you still want a rigid MVC layering then?

Erik Kaplun
  • 481
  • 4
  • 12