Modeling Business Workflows as Code with Guará
Overview
In many software projects, there is a persistent gap between how business stakeholders describe a system and how developers implement it. Product Owners and analysts define workflows using business language, while developers translate those ideas into technical code. Testers then validate behavior using yet another layer of abstraction.
This separation introduces:
Miscommunication
Ambiguity
Misalignment between expectations and implementation
Even with practices like the Three Amigos, there is still a translation step between business intent and executable code.
This tutorial presents an alternative approach using the Guará framework: writing software where business language, implementation, and validation are aligned into a single expressive model.
Goal
By the end of this tutorial, you will understand how to:
Model business workflows using code
Use a shared language across business, development, and testing
Structure systems using reusable transactions
Extend a generic framework into a domain-specific language (DSL)
Reuse the same logic in both production and testing
Core Concept: Transactions
Guará is based on a simple idea:
Break your system into small actions (called transactions) and validate their results.
Each transaction:
Encapsulates a unit of behavior
Can return a result
Can be reused across multiple scenarios
Writing Readable Workflows
With Guará, you can express workflows using a structured, readable syntax:
app.given(UserLoggedIn) \
.when(Search, product="notebook") \
.then(ReportChart) \
.expects(IsEqualTo, 1)
This approach provides:
A clear execution flow
Improved readability
A structure that resembles natural language
At this stage, the code already improves test readability. However, it still uses generic terms.
Moving to Business Language
To fully align code with business thinking, you can extend the framework into a domain-specific language.
For example, in a financial context:
fin_app.account_with(HasBalance) \
.execute(BuyAsset, symbol="AAPL", amount=2000) \
.settles(UpdatePortfolio) \
.reconciles(ExpectedPortfolio, 20)
This version expresses:
Business intent
Domain terminology
Execution flow
The code is no longer just a test. It becomes executable business logic.
Implementing Transactions
Transactions encapsulate behavior and can be used in both production and testing.
Example:
class BuyAsset(AbstractTransaction):
def do(self, symbol, amount):
DataBase.balance -= amount
This implementation represents a real business operation:
A user buys an asset
The system deducts the corresponding amount from the balance
Because transactions are reusable, they can be used across:
Application logic
Test scenarios
Simulations
This eliminates duplication and ensures consistency.
Framework Architecture
The framework is built around three core components:
Transactions
Small, reusable units of work.
Example:
BuyAsset
UpdatePortfolio
Application
The orchestrator that executes transactions in sequence.
Assertions
Validation mechanisms that verify results.
Execution Flow
The framework follows a structured execution pattern:
given / when / then → execute transactions
asserts / expects → validate results
All execution is coordinated by the Application class.
Extending to a Domain-Specific Language
One of the key strengths of this approach is the ability to extend the framework into a ubiquitous language.
Generic Terms
given
when
then
asserts
Financial Domain Mapping
given → account_with, portfolio_with, positioned_with
when → execute, trade
then → settles, result_in
asserts → reconciles, balances
This mapping transforms technical steps into meaningful business expressions.
Example: Financial Application
Below is a simplified example of extending the framework for a financial domain:
from guara.transaction import Application, AbstractTransaction
from guara import it
class FinancialApplication(Application):
def __init__(self, driver=None):
super().__init__(driver)
def account_with(self, transaction, **kwargs):
super().given(transaction, **kwargs)
return self
def execute(self, transaction, **kwargs):
super().when(transaction, **kwargs)
return self
def settles(self, transaction, **kwargs):
super().then(transaction, **kwargs)
return self
def reconciles(self, assertion, expected):
super().asserts(assertion, expected)
return self
class BuyAsset(AbstractTransaction):
def do(self, symbol, amount):
DataBase.balance -= amount
class ExpectedPortfolio(it.IsEqualTo):
def __init__(self):
super().__init__()
def main():
fin_app = FinancialApplication()
(
fin_app.account_with(HasBalance)
.execute(BuyAsset, symbol="AAPL", amount=2000)
.settles(UpdatePortfolio)
)
def test_extend_ubiquitous_language():
fin_app = FinancialApplication()
(
fin_app.account_with(HasBalance)
.execute(BuyAsset, symbol="AAPL", amount=2000)
.settles(UpdatePortfolio)
.reconciles(ExpectedPortfolio, 20)
)
Key Benefits
Alignment Between Roles
Developers, testers, analysts, and Product Owners can all understand the same code.
Reusability
Transactions are reusable across:
Tests
Production workflows
Simulations
Readability
Code expresses intent clearly, reducing the need for external documentation.
Maintainability
Changes in business logic are localized within transactions.
Living Documentation
Test scenarios double as documentation of system behavior.
From Testing to System Design
What starts as a testing pattern evolves into a broader architectural approach:
Modeling workflows as code
Defining domain behavior explicitly
Bridging the gap between business and implementation
Instead of writing code and then explaining it, you write code that already explains itself.
Conclusion
When code reflects the language of the business:
Communication improves
Complexity is reduced
Systems become easier to evolve
If your code cannot be understood by the business, only part of the team can effectively work with it.
If your code speaks the language of the business, you are not just implementing features.
You are modeling reality.