Modeling a CLI Education Platform with Guará

Introduction

This article presents a complete example of how to build a CLI-based education platform using the Guará framework. The goal is to demonstrate how business requirements can be translated directly into executable use cases, and how those same use cases drive implementation and testing.

Instead of separating requirements, application logic, and tests, Guará allows you to express everything through a unified approach based on transactions and fluent scenarios. This results in a system where behavior is explicit, traceable, and aligned with business intent.

Problem Overview

The system models a simple academic environment with the following core concepts:

  • Students

  • Courses

  • Subjects

  • Enrollments

  • Grades and GPA

The platform supports operations such as:

  • Creating students, courses, and subjects

  • Enrolling students in courses and subjects

  • Assigning grades

  • Calculating GPA

All interactions are performed via CLI, but internally represented as Guará transactions.

Use Cases as the Source of Truth

In Guará, behavior is expressed using a fluent syntax:

eduapp.when(CreateStudent, with_name="John").asserts(it.IsNotNone)

eduapp.when(CreateCourse, with_name="Math").asserts(it.IsEqualTo, "Math")

eduapp.when(CreateSubject, with_name="Algebra", in_course_with_id="C1") \
      .asserts(it.IsEqualTo, "Algebra")

eduapp.when(EnrollStudentInCourse, student_id="S1", course_id="C1") \
      .asserts(it.IsTrue)

eduapp.when(SetGrade, student_id="S1", subject_id="SUB1", with_grade=8) \
      .asserts(it.IsTrue)

These use cases are not just tests. They define what the system must do. The implementation follows directly from them.

Domain Model

The domain is intentionally simple and focuses on business rules.

class Student:
    """Represents a student."""

    def __init__(self, nui=None, name=None):
        self.nui = nui
        self.name = name
        self.course = None
        self.subjects_grade = []
        self.status = None

    def add_grade(self, grade):
        self.subjects_grade.append(float(grade))

    def gpa(self):
        if not self.subjects_grade:
            return 0
        return sum(self.subjects_grade) / len(self.subjects_grade)

Persistence Layer

The repository persists data using SQLite. It abstracts the storage from the rest of the system.

class Repository:
    def __init__(self, db_path="education.db"):
        self.conn = sqlite3.connect(db_path)

    def add_student(self, nui, name, course_id=None):
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT INTO students (nui, name, course_id) VALUES (?, ?, ?)",
            (nui, name, course_id)
        )
        self.conn.commit()

    def get_student(self, nui):
        cursor = self.conn.cursor()
        cursor.execute("SELECT nui, name, course_id FROM students WHERE nui = ?", (nui,))
        return cursor.fetchone()

Transactions

Each use case is implemented as a transaction. This is where business behavior lives.

from guara import AbstractTransaction

class CreateStudent(AbstractTransaction):
    def do(self, repo: Repository, with_name):
        nui = len(repo.list_students()) + 1
        repo.add_student(nui, with_name)
        return True
class EnrollStudentInCourse(AbstractTransaction):
    def do(self, repo: Repository, student_id, course_id):
        repo.update_student_course(student_id, course_id)
        return True
class SetGrade(AbstractTransaction):
    def do(self, repo: Repository, student_id, subject_id, with_grade):
        repo.add_grade(student_id, subject_id, with_grade)
        return True

Application Layer

Guará provides an Application object that orchestrates transactions.

from guara.application import Application

eduapp = Application()
eduapp.execute(CreateStudent, repo=repo, with_name="John")

The Application instance manages execution, state, and undo operations when needed.

CLI Integration

The CLI acts as an entrypoint that maps user commands to Guará use cases.

def main():
    parser = argparse.ArgumentParser()

    parser.add_argument("--action", required=True)
    parser.add_argument("--name")
    parser.add_argument("--student")
    parser.add_argument("--course")
    parser.add_argument("--subject")
    parser.add_argument("--grade", type=float)

    args = parser.parse_args()

    repo = Repository()
    eduapp = Application()

    if args.action == "create_student":
        eduapp.when(CreateStudent, repo=repo, with_name=args.name).expects(it.IsTrue)

    elif args.action == "enroll_course":
        try:
            (
                eduapp
                .given(HasCourse, repo=repo, course=args.course)
                .and_(HasStudent, repo=repo, student=args.student)
                .and_(IsNotStudentEnrolledInACourse, repo=repo, student_id=args.student)
                .when(EnrollStudentInCourse, repo=repo, student_id=args.student, course_id=args.course)
                .asserts(it.IsTrue)
            )
        except Exception as e:
            print(str(e))
            eduapp.undo()

This design keeps the CLI thin and delegates all logic to transactions.

Testing with the Same Use Cases

Tests reuse the same transactions and scenarios.

def test_create_student():
    app = Application()
    app.when(CreateStudent, repo=repo, with_name="John").asserts(it.IsNotNone)
def test_enroll_course():
    app = Application()
    app.when(
        EnrollStudentInCourse,
        repo=repo,
        student_id="S1",
        course_id="C1"
    ).asserts(it.IsTrue)

There is no need for a separate testing DSL. The same language is used everywhere.

Benefits of This Approach

This architecture provides several advantages:

  • Single source of truth for system behavior

  • High readability aligned with business language

  • Reduced duplication between requirements and tests

  • Faster feedback loop during development

  • Easier onboarding for new developers and stakeholders

Conclusion

This project shows how Guará enables a different way of building systems. By modeling behavior as executable use cases, the gap between requirements, implementation, and testing disappears.

The result is a system where:

  • Use cases define the architecture

  • Transactions implement behavior

  • Assertions validate outcomes

This approach is particularly effective for business-driven applications where clarity and correctness are more important than technical complexity.

For more details, refer to the example: