Why Functional Decomposition Leads to Bad System Design
As we move forward, we need to be mindful of following the step-by-step approach: First, learn the rules of system design as stated by Juval. Second, follow those rules diligently. And finally, after extensive practice and once you're confident, bend the rules when appropriate. To ensure we thoroughly understand these principles, we will discuss each rule one by one in upcoming articles. We’ll also keep summarizing and listing them together so that when we start designing real-world software systems, we can validate our designs against this checklist. Let’s start diving deeper. If you haven’t read the previous blog, feel free to check it out—it’s linked in the conclusion of this article. System decomposition should show how components interact A good architect breaks a software system into smaller components during the design phase. A strong design clearly shows how these components interact. Poor decomposition can result in a system that's hard to maintain and extend in the future. In system design, when we talk about components, we mean services—which may or may not be separately deployed microservices. (Do not confuse this with microservices architecture.) We are not referring to classes and interfaces here, as their internal organization can be refactored later with relatively less effort. However, once a system’s high-level design is built, it’s far more difficult to revise. Avoid functional decomposition Functional decomposition couples a service with one or more requirements, which is a sign of a complex and poorly designed system. Such coupling prevents component reuse across different requirements, and it can lead to tight coupling with clients or other services—introducing complex dependencies. We’ll explore this in more detail with examples later, but here’s a sneak peek: Most of us engineers were taught to design systems based on functionality—especially in college. I remember attending OOP courses where we were told to divide systems by the tasks components perform. At a hackathon in college, for example, we built a food delivery app and split it into services like Billing Service, Shipping Service, and Order Service. We even won silver! But looking back, it was a clear case of bad decomposition based on functional separation. In that food delivery system: Order: where users add items and creates order Billing: where payment is handled Shipping: where shipping is initiated and monitored In functionally decomposed systems, services are executed in a rigid sequence—A (Order), B (Billing), C (Shipping). This introduces tight coupling. For instance, if you want to reuse the Billing Service (B) for monthly subscriptions without needing Order (A) or Shipping (C), it won’t work. The billing logic expects those services to exist and execute before or after it. That’s why such services aren't truly reusable. To make functional decomposition work, the Billing Service now needs to know about who’s calling it. That means it contains logic about other services—resulting in tight coupling. Over time, the billing service bloats and becomes hard to maintain or extend. That’s a dangerous monolith. Alternatively, you might try creating multiple billing services for different use cases. But that just leads to an explosion of services, which is another bad design. The client should be the client—not the system You might consider letting the client handle the sequence and logic of service calls. But then your client becomes bloated with system logic. Now, the client has to know about all the services, how to call them, and how to handle their errors. Any change in business logic—say, in the Billing Service—would require changes in the client code. That’s a nightmare for teams working in different domains. As you can see, functional decomposition either leads to too many services or to a few bloated ones. We must strike a balance. The client should not have multiple entry points into the system Having multiple separate entry points (e.g., services A, B, and C accessed individually by the client) adds complexity. Each entry point must handle transaction management, error handling, and client validation—again and again. This redundancy increases both effort and cost, especially when adding new clients or making system changes. A single, unified entry point simplifies the architecture. I remember in the hackathon we built a separate microservice for user authentication. The client was responsible for authenticating, then creating the order, and so on. It seemed fine then, but it was a bad design in hindsight. In reality, each system starts bloating to accommodate every other’s demands and errors. This becomes unmanageable very quickly. Conclusion We’ve now seen why functional decomposition leads to bad system design. It’s easy to fall into the trap of dividing systems based on what each part does—but that leads to unnecessary co

As we move forward, we need to be mindful of following the step-by-step approach:
- First, learn the rules of system design as stated by Juval.
- Second, follow those rules diligently.
- And finally, after extensive practice and once you're confident, bend the rules when appropriate.
To ensure we thoroughly understand these principles, we will discuss each rule one by one in upcoming articles. We’ll also keep summarizing and listing them together so that when we start designing real-world software systems, we can validate our designs against this checklist.
Let’s start diving deeper.
If you haven’t read the previous blog, feel free to check it out—it’s linked in the conclusion of this article.
System decomposition should show how components interact
A good architect breaks a software system into smaller components during the design phase. A strong design clearly shows how these components interact. Poor decomposition can result in a system that's hard to maintain and extend in the future.
In system design, when we talk about components, we mean services—which may or may not be separately deployed microservices. (Do not confuse this with microservices architecture.) We are not referring to classes and interfaces here, as their internal organization can be refactored later with relatively less effort. However, once a system’s high-level design is built, it’s far more difficult to revise.
Avoid functional decomposition
Functional decomposition couples a service with one or more requirements, which is a sign of a complex and poorly designed system. Such coupling prevents component reuse across different requirements, and it can lead to tight coupling with clients or other services—introducing complex dependencies.
We’ll explore this in more detail with examples later, but here’s a sneak peek:
Most of us engineers were taught to design systems based on functionality—especially in college. I remember attending OOP courses where we were told to divide systems by the tasks components perform. At a hackathon in college, for example, we built a food delivery app and split it into services like Billing Service, Shipping Service, and Order Service. We even won silver! But looking back, it was a clear case of bad decomposition based on functional separation.
In that food delivery system:
- Order: where users add items and creates order
- Billing: where payment is handled
- Shipping: where shipping is initiated and monitored
In functionally decomposed systems, services are executed in a rigid sequence—A (Order), B (Billing), C (Shipping). This introduces tight coupling. For instance, if you want to reuse the Billing Service (B) for monthly subscriptions without needing Order (A) or Shipping (C), it won’t work. The billing logic expects those services to exist and execute before or after it. That’s why such services aren't truly reusable.
To make functional decomposition work, the Billing Service now needs to know about who’s calling it. That means it contains logic about other services—resulting in tight coupling. Over time, the billing service bloats and becomes hard to maintain or extend. That’s a dangerous monolith.
Alternatively, you might try creating multiple billing services for different use cases. But that just leads to an explosion of services, which is another bad design.
The client should be the client—not the system
You might consider letting the client handle the sequence and logic of service calls. But then your client becomes bloated with system logic. Now, the client has to know about all the services, how to call them, and how to handle their errors. Any change in business logic—say, in the Billing Service—would require changes in the client code. That’s a nightmare for teams working in different domains.
As you can see, functional decomposition either leads to too many services or to a few bloated ones. We must strike a balance.
The client should not have multiple entry points into the system
Having multiple separate entry points (e.g., services A, B, and C accessed individually by the client) adds complexity. Each entry point must handle transaction management, error handling, and client validation—again and again.
This redundancy increases both effort and cost, especially when adding new clients or making system changes. A single, unified entry point simplifies the architecture.
I remember in the hackathon we built a separate microservice for user authentication. The client was responsible for authenticating, then creating the order, and so on. It seemed fine then, but it was a bad design in hindsight.
In reality, each system starts bloating to accommodate every other’s demands and errors. This becomes unmanageable very quickly.
Conclusion
We’ve now seen why functional decomposition leads to bad system design. It’s easy to fall into the trap of dividing systems based on what each part does—but that leads to unnecessary complexity and tight coupling.
In the upcoming articles, which I publish every Sunday, we’ll explore anti-design patterns and how to avoid functional decomposition effectively.
Stay tuned!
Find the link to the previous article in the conclusion below in case you missed it.