Software Architecture: From Chaos to Elegance

Software Architecture: From Chaos to Elegance

In our last blog, we shared how we inherited a messy codebase from another company, re-examined the client’s needs, and implemented a new architecture that aligned with their vision. Today, we’ll dive deeper into the process of analyzing requirements, making observations, defining modules, and ultimately designing an architecture that brought clarity and scalability to a complex app.

The challenge before us was daunting: a matchmaking app rooted in Arabic culture, featuring over 100 unique questions across categories such as family, work, and personal preferences. These questions weren’t simple either—some had dropdowns, others involved range selections, text inputs, or condition-based subquestions that linked dynamically to other queries. Additionally, the app incorporated a timer-based flow to encourage users to approach matches with care and deliberation, reflecting the traditional matchmaking practices of a khattabah (matchmaker).

Let’s take a closer look at how we tackled this challenge.


The Problem with the Old System

When we analyzed the old codebase, it was clear that the previous team had taken a brute-force approach. Here’s what we found:

  1. 100+ Forms for Question Sets: Each question set had its own form, and there was a separate screen for every “step” in the matchmaking flow.
  2. Hardcoded Logic: The forms were rigid and inflexible. There were no tuneable parameters, making it impossible to adapt the app without rewriting significant portions of the code.
  3. Complex Flow: The matchmaking flow used timers for each step, but the logic for handling these timers was deeply embedded, making the system fragile and difficult to debug.

While this approach may have seemed logical during development, it was a nightmare for managing data, adding new features, or updating questions. Flexibility and scalability were completely absent.


Our Approach: A Better Way

To address these issues, we focused on two core elements central to the app:

  1. The Question Sets
  2. The Matchmaking Flow

We approached these with a mix of set theory, state machines, and modular design. Here’s how it unfolded.


Defining the Question Sets

The first step was to ask a deceptively simple question: What is a question?

Breaking Down Questions

We defined a question as an object linked to answers, which in turn represented the inputs expected by the question. This allowed us to create a modular and flexible system:

  • Answers: Each answer object defined the type of input expected. For example, dropdowns, sliders, or text inputs.
  • Gender-Specific Inputs: Some questions had different input types based on gender. We handled this by linking a single question object to multiple answer objects.
  • Responses: These objects stored user inputs and linked them to their corresponding answer objects.
  • Categories: Questions were grouped into overarching categories (e.g., family, work) for better organization.

Standardizing Input Types

We analyzed all question sets and identified a fixed number of input types. These input types were then grouped into reusable components, each with tuneable properties to cater to specific needs. For example:

  • A range slider component for selecting values within a range.
  • A dropdown component for predefined choices.
  • A text input component for open-ended responses.

By standardizing input types, we created a system where all question sets could be composed of these modular components. This approach significantly reduced complexity and made the app much easier to maintain.

Defining Input Spaces with Set Theory

To make the system even more robust, we turned to set theory. Each input type was treated as a set, and responses were elements of these sets. This allowed us to:

  • Perform set operations (union, intersection, etc.) on responses.
  • Ensure consistency and predictability in matchmaking.
  • Simplify the logic by focusing on input types rather than individual questions.

The results were game-changing. Matchmaking now worked seamlessly on well-defined sets, making the logic clearer, faster, and more scalable.

Widget Renderer

We implemented the question sets using a custom Widget Renderer that dynamically generated input components based on JSON definitions. This made it easy to:

  • Add new questions.
  • Update existing ones.
  • Maintain a clean and modular codebase.

Look out for an upcoming blog where we’ll dive deeper into how the Widget Renderer works.


Redefining the Matchmaking Flow

The matchmaking flow was another area ripe for improvement. The old system relied heavily on if-else statements and timers. While the logic technically worked, it was cumbersome and prone to errors.

Flowchart to the Rescue

We started by creating a detailed flowchart of the matchmaking process. This helped us visualize:

  • All the steps involved.
  • The triggers for each step (e.g., timers ending, user actions).
  • The overall user journey.

Introducing a State Machine

Instead of hardcoding the flow logic, we implemented a state machine. Here’s how it worked:

  • States: Each screen or step in the matchmaking flow was represented as a state.
  • Triggers: Events like a timer ending or a user action acted as triggers to transition between states.
  • Timer as a Trigger: Rather than embedding timer logic directly, we treated the timer’s end as a trigger within the state machine. This made the flow more intuitive and easier to manage.

Mapping the entire flow to a state machine allowed us to:

  • Reduce complexity.
  • Ensure consistency in navigation and logic.
  • Make it easier to add or modify steps in the future.

Fewer Screens, Fewer Components

By consolidating screens and optimizing the flow, we drastically reduced the number of components and lines of code. The app became lighter, faster, and far easier to maintain.


The Result: Simplicity Meets Scalability

Our reimagined architecture brought order to chaos. Here’s what we achieved:

  1. Modular Question Sets:
    • Standardized input types made the system flexible and easy to update.
    • JSON-driven Widget Renderer ensured maintainability.
  2. Efficient Matchmaking Logic:
    • Set theory simplified response handling and improved performance.
    • State machines made the flow predictable and scalable.
  3. Cleaner Codebase:
    • Fewer screens, fewer components, and fewer lines of code.
    • Detailed documentation ensured future developers wouldn’t face the same challenges we did.

Lessons Learned

This project reinforced some key principles of software architecture:

  • Understand the Problem First: Take the time to analyze requirements and understand the client’s vision before diving into code.
  • Keep It Modular: Break down complex systems into reusable components and clear structures.
  • Embrace Formal Models: Techniques like set theory and state machines can bring clarity and predictability to complex systems.
  • Documentation is Critical: A well-documented system is easier to maintain and extend.

What’s Next?

We’ve only scratched the surface here. In upcoming blogs, we’ll dive deeper into:

  • The Widget Renderer: How we dynamically generated input components.
  • The Mathematics of Matchmaking: Using set theory for fast and accurate matches.
  • State Machines in Detail: A closer look at how we mapped the flow and implemented it in code.

Don’t forget to subscribe so you don’t miss these deep dives. And if you have a legacy app that needs rescuing, we’d love to help you transform it into a scalable success story.

Leave a Reply

Discover more from Highpolar Software

Subscribe now to keep reading and get access to the full archive.

Continue reading