Recognizing Policy
When writing apps, we often talk about “business logic” and treat it like special code. We work hard to isolate our business logic from other parts of the code, like the view layer or framework-specific code, and we say that if you have limited test resources, testing your business logic is the first priority.
Business logic is the core behavior of your app, but there’s a certain class of business logic that I believe warrants extra attention: Policy.
What is policy⌗
Apps are filled with policies. These are different than the standard business rules like “the send message button is disabled if the chat input field is blank”. What I have come to call “policy” is more of the arbitrary rules that are purely business decisions. Things like “only premium users can access the chat feature”.
The first example is something that an engineer writing a login screen could easily guess, since it makes logical sense. If that logic was missed on a client platform it would generally not be a big deal.
The second example is purely a business decision. Making chat a premium-only feature isn’t some obvious, logical choice, but a business decision that has to be explicitly chosen. Because of this, it’s important for it to be consistent across all of your app surfaces. And since this policy is simply a choice, it can change.
Policy changes⌗
Because policies are explicit decisions it’s easy to understand how they change when someone changes their mind, but there are other reasons for them to change, too. Before leaving my previous role, I was working on solutions for a policy change that was out of our control.
The app has a commenting feature, which essentially functions like chat. Users can comment on shared files and mention other users. Initially, this feature was available to everyone that had access to the file. This policy was easy, each platform simply created the comment feature and showed it along-side the file viewer.
Later, file-specific comment permissions were added, meaning that the file owner could decide whether others are welcome to comment on their file when they share it. Now each platform had to update it’s logic to check the specific file permissions to see if commenting was allowed.
Most recently, with the introduction of Age Gating laws, this chat-like commenting feature couldn’t be used by minors in certain jurisdictions. This, it turns out, is quite a bit of complexity for several platforms to get right, simply to determine if a user can access a feature.
This policy, whether a user can access the chat feature, is a perfect example of how business decisions and constraints can change over time. Additionally, it shows how policies can actually be quite complex, with multiple contributing factors.
Centralizing policy⌗
Due to the importance of getting this policy right across all of our platforms, I advocated for and started building a centralized policy service, specifically for feature access policies. With this, instead of sprinkling our policy logic throughout the product code on all platforms, we were able to have a single, testable service that could answer the question “should this user have access to this feature”.
One option for building this kind of policy engine is to use something like the Decider pattern that Jesse described in his 2024 post “Deciders, Decisions and Executors”, and that’s exactly how the first iteration of the system I designed worked. Stealing some functional programming ideas, a policy is really just a decision that is being made based on some set of inputs. This means that a simple decide() function can contain the logic, and the transform function shape means that creating a comprehensive test suite is quite straight-forward.
This kind of policy definition, due to it’s self contained structure, is a great place to introduce cross platform code sharing. The well defined functional API surface of inputs and outputs means that multi-platform code can be tested in isolation and trusted to run on client platforms, without requiring significant refactoring of consuming code.
For a flexible, portable server-side solution, another option is something like Cedar, which is a language for defining permissions as policies, and a specification for evaluating those policies. The Cedar language makes defining policies easy, and there are various runtime implementations that are extremely fast at evaluating those policies. Additionally, the definitions can be easily analyzed to identify dead paths, conflicts, and other kinds of problems.
Conclusion⌗
Policies are a special kind of business logic that require special care. When complex logic needs to be trustworthy and identical across platforms, centralizing and sharing the definition of that logic is a great way to reduce risk and allow for flexibility of business decisions.