Migrating a Legacy System to Page Transactions with Guará

This guide explains how to gradually migrate a real-world system from a procedural or poorly structured codebase to the Page Transactions pattern using Guará, without breaking the system or requiring a full rewrite.

We will use a simple bakery management system as an example, with operations like:

  • Add product to stock

  • Sell product

  • Remove product

  • Emit bills

The goal is to move safely, step by step, while keeping the system working at all times.


Step 1: Identify Current Use Cases (Do Not Refactor Yet)

Start by identifying what the system does today.

Example of legacy procedural code:

def sell_product(stock, product_name, quantity):
    if product_name not in stock:
        return "Product not found"

    if stock[product_name] < quantity:
        return "Not enough stock"

    stock[product_name] -= quantity
    return "Sale completed"

At this stage:

  • Do not refactor

  • Do not change behavior

  • Just list the main use cases

Examples:

  • Sell product

  • Add product to stock

  • Remove product

  • Emit bill


Step 2: Write Use Cases in Guará Style (Even Before Refactoring)

Now, describe the behavior using Guará syntax.

app.when(SellProduct, product_name="Bread", quantity=2) \
   .asserts(it.IsEqualTo, "Sale completed")

At this point:

  • The transaction may not exist yet

  • You are defining intent first

This becomes your target behavior


Step 3: Wrap Existing Code into Transactions (No Logic Change)

Create transactions that simply call the existing procedural code.

from guara import AbstractTransaction

class SellProduct(AbstractTransaction):
    def do(self, stock, product_name, quantity):
        return sell_product(stock, product_name, quantity)

Important:

  • Do not improve the code yet

  • Just wrap it

This gives you:

  • Immediate compatibility with Guará

  • Zero risk migration


Step 4: Introduce Application Context

Move shared data (like stock) into an application object.

class BakeryApp:
    def __init__(self):
        self.stock = {
            "Bread": 10,
            "Cake": 5
        }

Update transaction:

class SellProduct(AbstractTransaction):
    def do(self, product_name, quantity):
        return sell_product(self.app.stock, product_name, quantity)

Now the system starts to become structured.


Step 5: Start Writing Tests Using Transactions

Use the same use cases as tests.

app = Application(BakeryApp())

app.when(SellProduct, product_name="Bread", quantity=2) \
   .asserts(it.IsEqualTo, "Sale completed")

At this point:

  • You already have tests

  • Without rewriting logic


Step 6: Gradually Refactor Inside Transactions

Now that behavior is protected by tests, improve the code safely.

Replace procedural logic:

class SellProduct(AbstractTransaction):
    def do(self, product_name, quantity):
        product = self.app.stock.get(product_name)

        if not product:
            return "Product not found"

        if product < quantity:
            return "Not enough stock"

        self.app.stock[product_name] -= quantity
        return "Sale completed"

You can:

  • Remove legacy functions

  • Improve naming

  • Add validations


Step 7: Split Large Transactions (If Needed)

If a transaction becomes too big, break it.

class HasStock(AbstractTransaction):
    def do(self, product_name, quantity):
        stock = self.app.stock

        if product_name not in stock:
            raise OutOfStockException("No product in stock")

        if stock[product_name] < quantity:
            raise OutOfStockException("Not enough product in stock")

Reuse it:

class SellProduct(AbstractTransaction):
    def do(self, product_name, quantity):
        self.app.stock[product_name] -= quantity
        return "Sale completed"

Step 8: Improve Readability with Guará Syntax

Move from simple calls to fluent scenarios:

app \
  .given(HasStock, product_name=product_name, quantity=quantity) \
  .when(SellProduct, product_name="Bread", quantity=2) \
  .asserts(it.IsEqualTo, "Sale completed") \
  .when(EmitBill, product_name="Bread", quantity=2) \
  .asserts(it.Contains, "Bill emitted")

Now your code:

  • Reads like a business flow

  • Is executable

  • Is testable


Step 9: Replace CLI / API with Transactions

Old CLI:

if action == "sell":
    sell_product(stock, name, qty)

New CLI:

app.when(SellProduct, product_name=name, quantity=qty)

Your interface becomes a thin layer.


Step 10: Organize Code by Intent

Group transactions:

  • actions/ → SellProduct, AddStock

  • validations/ → HasStock

  • queries/ → ListProducts

This improves scalability.


Step 11: Remove Legacy Code

Once everything is covered by transactions and tests:

  • Remove old procedural functions

  • Keep only transactions and domain logic


Final Result

You moved from:

sell_product(stock, "Bread", 2)

To:

app.when(SellProduct, product_name="Bread", quantity=2)

With:

  • Unified requirements, implementation, and tests

  • Safer refactoring

  • Better readability

  • Scalable architecture


Key Migration Principles

  • Do not rewrite everything at once

  • Wrap first, refactor later

  • Use transactions as boundaries

  • Protect behavior with assertions

  • Improve incrementally


Final Thought

You don’t adopt Guará by rewriting your system.

You adopt it by wrapping your system with use cases, then letting those use cases reshape your architecture over time.