Modeling a simple use-case in NPL
Learn how to model and implement a document approval process using NPL core constructs, state machines, and multi-party workflows.
Prerequisites
- Be comfortable with basic Git operations, such as cloning repositories and navigating project directories
- Be familiar with basic concepts of object-oriented programming, such as classes, attributes, and methods
- Have completed the Developing NPL on your own machine track or the Developing NPL in GitHub Codespaces track
- Have completed the Creating a new NPL project track
Learning outcomes
After this track, you'll be able to:
- Understand the NPL paradigm: Differentiate NPL from traditional programming languages and recognize its strengths in modeling multi-party workflows
- Master NPL's core constructs: Learn key constructs such as protocols, permissions, and parties, and their role in enforcing business rules
- Adopt the NPL mindset: Approach development with a contract-driven, state-oriented perspective aligned with NPL's declarative nature
- Build real-world models: Translate complex processes into scalable and transparent NPL workflows
- Use NPL tooling: Efficiently leverage IDE features, documentation, and testing frameworks to accelerate development
This walkthrough describes the key concepts with a bank account modelling. It lets you explore NPL by implementing a document approval process in the hands-on parts.
Problem introduction: building a document approval process
The document approval process
In this guide, you will model and implement a document approval process using the Noumena Protocol Language (NPL).
Initially, a document editor creates a document and edits it. Then, the document editor requests an approval. The approver reviews the document and approves it. This creates a 4-eyes check for the document involving two different users.
The process includes the following steps:
Walkthrough
This walkthrough will guide you step-by-step through the process of modeling a document approval process.
If you need help at any point, check out the NPL documentation. Syntax helpers along the exercises point to relevant sections of the documentation. A convenient NPL cheat sheet is also available with the most important information.
Repository setup
Theory
A simple but important thing to always keep in mind when setting up an NPL project is to set the source directories. This will allow the NPL Plugin in your IDE to recognise the source files.
Practice
Make sure you have completed the following tracks:
- The Developing NPL on your own machine track or the Developing NPL in GitHub Codespaces track
- The Creating a new NPL project track
In the Creating a new NPL project track, you created a new NPL project. Open this project in your IDE.
You should now be able to run a command to verify that the project is set up correctly.
npl test --sourceDir api/src/main/npl
This should return no errors.
Packages
Theory
In NPL, packages are used to create a separation of concerns. Each package relates to a specific domain of the business problem, set of processes or data containers conceptually related.
The business modelling within a package is done using protocols. They represent objects, processes or data containers. To model a business problem, you will need to create one or more protocols. It is good practice to isolate each protocol in its own file.
Each protocol file is associated to a package via an explicit declaration within the file itself: package packageName
,
in addition to their location within the file system.
Reference:
Practice
In the main/npl
folder, create a new folder named document-approval
to represent the package for the document
approval process.
Within the document-approval
package, create a new file called document.npl
In document.npl
, add the package declaration: package document-approval
Protocols
The first step in the implementation of the document approval process is to model the template for the document process. In NPL, this is done using a protocol. For this example, we have a simple process that allows a document editor to edit the document content and a document approver to approve the document.
The relatively simple document approval process can be modelled using a single protocol. For more complex processes, it is good practice to create multiple protocols, using protocol references to compose them. To split a process into multiple protocols, it is good practice to use a sequence diagram or a use case diagram to isolate the different concerns, a sub-process, or different actors.
Theory
Protocols are the core constructs of NPL, they are a template for an interaction (around a process, around a set of data, etc.). Protocols define such interactions in terms of permissions and obligations, and bind together the different parties and their actions. Protocol definitions do not pertain to specific parties, but rather define parties in terms of roles such as 'editor' and 'approver' in a very specific context.
Conceptually, protocols are somewhat similar to classes from object-oriented programming. When modeling business logic in NPL, protocols are particularly useful to represent templates that:
- will be created multiple times,
- have an own complex lifecycle, and
- can be accessed by multiple parties, each with different permissions.
The header of a protocol definition starts with the keyword protocol
, and is immediately followed by the protocol
parties. The protocol name and its constructor parameters follow immediately after.
Here is an example of a protocol definition for a bank account:
package bank;
@api
protocol[account_holder, bank] Account(
var accountHolderName: Text
) {
// Protocol body
}
As you are editing, the NPL-Dev plugin gives you syntax highlighting and underlines syntax errors (e.g. malformed protocol header, missing semicolon).
Reference:
Practice
We will now create a Document
protocol to streamline the interaction between the document editor
and the document
approver
. It will enable the document editor to specify and edit the document content, and the document approver to
approve the document. We will also get some help from the NPL-Dev plugin and verify that the code compiles correctly.
The parties defined here are the roles that users can play in the document approval process. It is not yet defined which user(s) will play which role.
Note
Parties are a type of variable in NPL. They are declared in the protocol header, and can be used in the protocol body. The party variables are assigned to the parties when the protocol is instantiated, from other party variables or a set of claims.
For more information on parties, see Parties.
In the newly-created file document.npl
, copy-paste the structure of the Account
protocol:
package bank;
@api
protocol[account_holder, bank] Account(
var accountHolderName: Text
) {
// Protocol body
}
Then adjust it to match the document approval process:
- Define the two parties that will have access to the protocol -
editor
andapprover
. Parties here should be comma-separated - Name the protocol
Document
- Define an attribute for the constructor:
content
, of typeText
. The attribute should be explicitly declared as a variable (var
), so that it is stored and can be re-used as a protocol field after instantiation - Remove the comma between
editor
andapprover
. Note that the NPL parser will highlight an error - Re-insert the comma separating the two parties to come back to a valid protocol declaration
Solution
protocol[editor, approver] Document(
var content: Text
) {
}
Permissions
Following the declaration of the Document
protocol as the template for the document approval process, we need to
define the actions that the different parties can perform on the document. Each user action can be undertaken by
specific people only, and for some actions, only at specific times. Using a sequence diagram or a use case diagram, we
can identify the different actions and the different parties involved. For each user action, we need to define a
permission.
Theory
Permissions represent actions users can perform in relation to a protocol. They are declared within the protocol body. The declaration specifies what parties can invoke the permission, the input parameters, whether the permission should return any value, and potential restrictions on invoking the permission (which will be discussed later on).
In our bank account example, we can create a permission to allow the bank to load funds into an account. This permission
can be invoked by the user playing the bank
role only.
package bank;
@api
protocol[account_holder, bank] Account(
var accountHolderName: Text
) {
var balance: Number = 0;
permission[bank] loadFunds(amount: Number) {
// Permission body
}
}
Reference:
Practice
Within the Document
protocol, we are going to create an edit permission, which takes as parameter the new content. The
goal of this permission is to allow the document editor to edit the document content.
Within Document
, copy-paste the permission declaration from the bank account example above, and adjust it to match the
document approval process:
- Specify
editor
as the party that will have access to the permission - Name the permission
edit
- Define the input parameter:
newContent
, of typeText
- Assign the new content to the content field
Solution
protocol[editor, approver] Document(
var content: Text
) {
permission[editor] edit(newContent: Text) {
content = newContent;
}
}
States
Along the designed process, some actions are only allowed at specific times. For example, the document editor can edit the document content at the beginning of the process, and not anymore after requesting an approval. This is where states come in. Try identifying the differences in behaviour along the process, or for your data objects. If there is clear progress in the sequence of actions that are needed, this can be modelled using states.
Theory
NPL allows to track the progress of processes through protocol state machines. State machines are composed of states, one being active at any given time. One of the states must be an initial state. It is also possible to define final states, meaning that once they are reached the state of the protocol cannot be changed anymore. Multiple states may be final.
package bank;
@api
protocol[account_holder, bank] Account(
var accountHolderName: Text
) {
initial state active
final state frozen
// Protocol body
}
To illustrate the progress within a process, a state transition is declared with the keyword become
followed by the
new state. It is also possible to define state guards for protocol actions such as permissions. This is done using the
pipe symbol |
following the optional return type, and followed by a comma-separated list of protocol states. This
state guard ensures that the permission or obligation can only be called in the listed state(s). If no state guard is
specified, the permission or obligation is allowed in all states.
package bank;
@api
protocol[account_holder, bank] Account(
var accountHolderName: Text
) {
initial state active
final state frozen
var balance: Number = 0;
permission[bank] loadFunds(amount: Number) | active {
balance = balance + amount;
if (balance >= 10000) {
become frozen;
}
}
}
Reference:
Practice
We can track the state of the document approval process. The document can take three states: an initial state created
,
a state inReview
and a final state approved
. We can use state guards to define when an action is allowed.
Within the Document
protocol, add three states at the beginning of the protocol body:
-
Add three states at the beginning of the protocol body:
- an initial state
created
- a state
inReview
- a final state
approved
- an initial state
-
Update the edit permission to use state guard and ensure the protocol is in the
created
state when the permission is called. - Create a new permission
requestReview
that allows the editor to request an approval. This permission should transition the protocol to theinReview
state. - Create a new permission
approve
that allows the approver to approve the document once it is in stateinReview
. This permission should transition the protocol to theapproved
state.
Solution
protocol[editor, approver] Document(
var content: Text
) {
initial state created
state inReview
final state approved
permission[editor] edit(newContent: Text) | created {
content = newContent;
}
permission[editor] requestReview() {
become inReview
}
permission[approver] approve() | inReview {
become approved
}
}
Tests
We now have an initial behaviour for the document approval process. We can test it to ensure that it works as expected, and avoid breaking the process later on.
For more advanced developers, implementing tests comes before developing the business logic.
Theory
NPL offers a default testing framework to run unit tests. The framework is based on test functions. It is recommended for the test file to be in the same package as the file being tested (as specified by the package statements in the respective source files).
Each test function is marked via a @test
decorator. The function takes an object of type Test
as input, which will
be used to call Test methods throughout the execution of the test.
@test
function testEqPassing(test: Test) -> {
test.assertEquals(someValue, theSameValue, "informative message");
}
Here’s an overview of some of the most important Test
methods.
-
assertTrue
: Asserts that the provided value is true. Optionally a message may be provided that is displayed instead of the default message.test.assertTrue(conditionThatIsTrue, "The pump should have been turned on.");
-
assertEquals
: Asserts that the provided value is equal to the expected value. Optionally a message may be provided that is displayed instead of the default message.test.assertEquals(someValue, theSameValue, "Account was not successfully updated");
-
comment
: Sets an additional message that will be printed during the test.test.comment("Test was successful");
We recommend familiarizing with the other Test
methods in the documentation.
The powerful aspect of NPL tests is that they make it easy to test not only logic, but also permissions. To see this in action, however, we first need to formally introduce the concept of parties in the next sections.
Reference:
Practice
Let’s set up template test functions that we will use to test the Document
protocol.
- Navigate to the
api/test/npl
folder - Create a
document-approval
module within the folder - Within the
api/test/npl/document-approval
module, create a filetest-document.npl
- Within the created file, set the package to
document-approval
-
Copy-paste the following template for the test function:
@test function createDocument(test: Test) -> { test.assertTrue(true, "The test is passed"); }
-
Similarly, create a template function
editDocument
: -
Run the tests by using the npl test command
npl test api/
Solution
package document-approval
@test
function createDocument(test: Test) -> {
test.assertTrue(true, "The test is passed");
}
@test
function editDocument(test: Test) -> {
test.assertTrue(true, "The test is passed");
}
Working with protocol instances
In the previous sections, we have created a protocol, described as templates for interactions. We now need to create protocol instances to represent the actual objects that can be used and interacted with. Going from the concept of a loan and its possible data and states to a specific loan with actual data values and state at some point in time, or from the concept of a document (including approval process) to a specific document with specific content and at a specific stage of the editing/approval at some point in time. Protocol instances can be envisioned as the digital counterparts of the physical objects that can be used and interacted with.
Theory
Protocols can be instantiated to create protocol instances. Protocol instances are the actual objects that can be used and interacted with. Instances represent a specific process or object, like a specific bank account or document.
Protocol instances are created by invoking the protocol constructor. The constructor takes the parties and the constructor arguments as input.
var account = Account[account_holder, bank]("Jane Doe");
Protocol instances can be used to invoke permissions and obligations.
account.loadFunds[bank](500);
Practice
Within the test for the Document
protocol, adjust the createDocument
test function:
- complete the test function to create a protocol instance
- complete the test function to invoke the
edit
permission and assert that the document content has been updated - create a test function to approve the document and assert that the document state has been updated to the
approved
state.
For the time being, use the 'editor'
and 'approver'
(with vertical quotes) values for the parties to create the
instance and invoke the permission.
Note
As we have created a protocol instance with the 'editor'
and 'approver'
parties, the 'editor'
party will be able to edit the document and the 'approver'
party will be able to approve the document.
Note
The active state of a protocol instance can be retrieved using the activeState().getOrFail()
methods on an instance.
account.activeState().getOrFail()
A specific state of a protocol instance can be retrieved using the States.stateName
attribute on the protocol type.
Account.States.frozen
Solution
@test
function createDocument(test: Test) -> {
var document = Document['editor', 'approver']("Hello");
test.assertEquals("Hello", document.content);
}
@test
function editDocument(test: Test) -> {
var document = Document['editor', 'approver']("Hello");
document.edit['editor']("Hello world");
test.assertEquals(document.content, "Hello world");
}
@test
function approveDocument(test: Test) -> {
var document = Document['editor', 'approver']("Hello");
document.requestReview['editor']();
document.approve['approver']();
test.assertEquals(document.States.approved, document.activeState().getOrFail());
}
Conclusion
Congratulations, you have successfully completed the hands-on session! We hope you’ve enjoyed this guide and found it useful.
Here's a summary of what you've learned today:
- Understand how to model a simple use case in NPL, specifically focusing on a document approval process
- Learn to create and implement protocols with attributes, permissions, and state transitions
- Implement permissions to enable interaction between protocol parties
- Utilize states and state guards to control protocol workflows effectively
- Create unit tests to verify protocol logic and permissions using the NPL testing framework
- Gain hands-on experience in integrating parties into protocols and managing their interactions
Next steps
- Follow the Using the NPL API with Swagger UI guide to learn how to interact with your NPL protocols via the Swagger UI
- Extended modelling of the document approval process
- Modelling multi-protocol workflows
- Parties in NPL