top of page

SNIPPETS LTD.

Anti-Patterns 02 Cleaning the Spaghetti Code

  • Writer: Pavol Megela
    Pavol Megela
  • Jan 16, 2023
  • 7 min read
ree

Angular is a powerful framework, but it’s not immune to the pitfalls of bad coding practices. Let's dive into one of the biggest headaches we face as developers: Angular spaghetti code. I’ll break down how these anti-patterns emerge and share practical tips on avoiding them. Let’s dive in and get our Angular projects so clean and manageable that they never resemble Eminem’s Mom’s spaghetti.


What is Spaghetti Code

Spaghetti code is a term used to describe poorly organized, tangled source code that is difficult to read, understand, and maintain. It often arises when a project grows without proper structure or design patterns or is simply being rushed, making future updates and debugging a real headache.


One real-world example is a e-commerce app I worked on, updating the discount calculation in the checkout process ended up breaking the shipping rates. The discount logic was tangled together with shipping calculations in the same module, so even a small tweak caused unexpected issues elsewhere. This tight coupling made changes risky and changes difficult, a classic case of spaghetti code.


How to Spot Spaghetti Code:


  • Lack of Modularity:

    Code that doesn’t follow a clear structure, with functions or classes that try to do too much instead of focusing on a single task.


  • Excessive Interdependencies:

    When different parts of the code are tightly coupled, making it hard to change one part without affecting many others. For example, when product, cart, and user logic are interdependent and scattered across various parts of the code.


  • Poor Readability:

    Inconsistent naming conventions, and a mix of different coding styles. Adding a new feature or writing tests requires understanding long methods or navigating through a maze of interdependent functions.


  • Unstructured Flow:

    The control flow is chaotic, with deeply nested loops, conditionals, and callbacks that resemble a tangled bowl of spaghetti.


  • Difficulty in Debugging:

    Finding bugs becomes challenging because the code lacks clear separation of concerns, and the logic is scattered throughout the file.


Spaghetti Code in an E-commerce Angular Application: A Real-World Example

Imagine an e-commerce web app that features product listings, seller profiles, customer reviews, and customer information. 


Chart of relationships between Components and Services in the example e-commerce app
Chart of relationships between Components and Services in the example e-commerce app

Let’s review the chart: the Product Component calls the ProductService (of course) to obtain product details. However, it also reaches out to the SellerService for seller information and the ReviewService for displaying reviews—a common use case where a component must load data from multiple services.

Similarly, the SellerProducts Component fetches seller data from the SellerService and then calls the ProductService to list the seller’s products. Notice that both components are now dependent on multiple services. Additionally, the ReviewService relies on the SellerService to enrich reviews—this is a clear red flag, as one service depends on another within the same layer.


This setup creates a circular dependency chain where changes in one service ripple through others. When you factor in the interrelationships between the models, we get a complete spaghetti meal, as shown in the image below.


Complete spaghetti, now including the relationships between models as well.
Complete spaghetti, now including the relationships between models as well.

As you can see, a Product is always linked to a Seller because every product is sold by someone. It also has Reviews, as customers provide feedback. Additionally, a Seller own a Products, meaning one seller can have one or many products.


We can see that this is a clear example of spaghetti code mess, due to tightly coupled services and components that are overly dependent on each other. And this is just a small-scale example with only three services and two components and we already have this mess... in a real e-commerce application, with more modules and features the complexity would escalate even further making maintenance and scalability much much harder.


A major problem with this setup is how tightly coupled everything is. For example, let’s say we change the sellerLogo field in the Seller model from a relative URL to an absolute URL. Since the Product Component displays seller information, including their logo, it would break if it assumes the logo is always a relative path. Similarly, the SellerProducts Component, which lists products for a seller, would also fail if it prepends a base URL incorrectly. And because the ReviewService enriches reviews with seller details, possibly displaying the seller’s logo next to their product reviews, it too would be affected.


This means a seemingly small change in one model forces updates across multiple services and components, making the system fragile. In a larger application, where more modules depend on shared data structures, such changes could require widespread modifications, increasing development time and the risk of introducing new bugs.


How to fix the Spaghetti mess we made

Refactored E-commerce Architecture: This diagram illustrates the improved structure where models are decoupled, services operate independently, and an abstraction layer (ProductStateService) orchestrates data fetching.
Refactored E-commerce Architecture: This diagram illustrates the improved structure where models are decoupled, services operate independently, and an abstraction layer (ProductStateService) orchestrates data fetching.

To fix our spaghetti mess, we’ll start by refactoring the models. The goal here is to eliminate direct interconnections between models. Each model should stand independently without tight coupling to other entities. This doesn't necessarily mean relationships disappear it means models shouldn’t directly reference each other in ways that would cause complexity.


The models will be updated in following way: Removing dependencies (avoiding tight coupling) means that models no longer directly embed full objects of other models.

Keeping relationships means that models still reference each other in a lightweight way, such as by storing only an ID or some minimal reference.


Next, we refactor the services. Currently, services directly reference and depend on each other. To address this, we introduce a clear abstraction layer. We can call this abstraction a ProductStateService, which acts as a central orchestrator. This state service will know about the lower-level services (Product, Seller, and Review) and will handle all coordination between them.


The ProductState abstraction then becomes responsible for:

  • Calling individual services independently, aggregating the data, and transforming it into a consistent format needed by the component.

  • Defining clear, stable contracts (often called Data Transfer Objects or DTOs) between itself and each service. For example, the ProductState might specify that when requesting Seller data, the SellerService always returns data in a certain agreed-upon format. Even if the internal Seller model changes, our previous example of switching the sellerLogo from a relative path to an absolute one, the SellerService would adhere to this contract. That way, the change in the underlying model won’t break components consuming this data, because they’re protected by the agreed-upon contract.


We should also consider using integration tests ensures that even when underlying implementations or data formats change, our ProductState maintains the integrity. These tests verify that each service correctly fulfils its contract, catching bugs early.


In summary:

  • Models become independent, not directly referencing each other.

  • A new abstraction layer (ProductState) orchestrates services and defines clear contracts, isolating model changes.

  • Integration tests guarantee stability, ensuring components won’t break due to underlying service changes.


By following this approach the architecture becomes clean, modular and maintainable.


Are we there yet?

Okay, that's one way to resolve our spaghetti architecture. However, we might look at our current solution and notice it's still not ideal because the ProductState now directly communicates with three separate services: ProductServiceSellerService, and ReviewService. Although this is better than before, we still have three direct dependencies converging into one layer, which could become problematic as the application grows.


To further refine this structure, we can introduce even more modularity and reduce direct dependencies. Instead of each service connecting directly to the ProductState, we can introduce another abstraction by creating a shared ApplicationState (or a GlobalState), see image below.


Further Refactored E-commerce Architecture: This diagram illustrates the even more refined structure where models are decoupled and dont know about each other using Application Hub/State.
Further Refactored E-commerce Architecture: This diagram illustrates the even more refined structure where models are decoupled and dont know about each other using Application Hub/State.

The ApplicationState acts as a central communication hub, allowing different modules (Product, Seller, Review) to communicate indirectly through events or minimal data exchanges. This can be achieved by clearly defining contracts, just as we've discussed before, where services agree on the minimal required data formats or events necessary for communication.


Here's how this refined structure would operate:

  • The individual states (e.g., ProductState, SellerState, and ReviewState) do not know about each other. Instead, they interact through the shared ApplicationState.

  • When ProductState detects a change (like an updated product detail), it dispatches an event or minimal necessary data to ApplicationState.

  • SellerState or ReviewState can then subscribe to these changes through clearly defined contracts. Whenever a change is published, the interested states update their local copies of this data. This duplication might seem counterintuitive at first, but it's common in distributed or decoupled systems. It optimizes for availability and consistency.

  • By using events or minimal data payloads instead of full domain models, we keep dependencies loose and ensure the individual states remain isolated and independent.


I understand this might be a bit confusing for some, so I'll briefly summarize it in a high-level flow example:

  1. User Action - The user submits a new review from the Review Module.

  2. Local State Update - The ReviewState handles validation and creation of the review, then notifies the ApplicationState that a new review was added, providing minimal relevant data (for example reviewId, productId).

  3. ApplicationState Dispatch - The ApplicationState receives the new review event and publishes it to any modules interested in updates. This might be via an Observable or an event bus.

Other Modules React:

  1. ProductState - sees a new review belongs to one of its products. It could choose to fetch more details.

  2. SellerState - might ignore the event entirely, unless we want to for example merge all products reviews in a Seller Profile, or display overall score.

  3. UI Updates - Each module’s local state triggers changes in their respective Angular components. Only the modules that care about the new review have to do anything.


Introducing ApplicationState helps manage complexity and creates a clear, predictable communication layer across your application. With this approach, we achieve better maintainability, scalability, and resilience against future changes.


Are we there yet?

Yes we are, I hope I didn't have you worried. In summary, we’ve seen how decoupling models, services, and modules, along with using an event-driven approach, can significantly reduce complexity in an Angular e-commerce application and get rid of Mom's Spaghetti. By introducing an abstraction layer or a global state bus, each feature can evolve independently without entangling other parts of the system.


If you’re interested in diving deeper, explore topics like state management libraries (e.g., NgRx, Akita), event-driven architecture (with RxJS), and domain-driven design practices. These areas will help you build robust, scalable, and maintainable front-end applications.

Comments


bottom of page