After more than a decade of building business applications, I noticed something interesting.
Very few projects became difficult because the business rules were complicated.
Most became difficult because of everything surrounding the business rules.
Every new module seemed to require another controller, another service, another repository, another set of CRUD methods, another search endpoint, another authorization check and another audit implementation.
Over time, the application became full of infrastructure code that looked remarkably similar across different modules.
That made me ask a simple question:
Can infrastructure be designed once so developers spend most of their time writing business logic instead of rewriting the same plumbing?
That question completely changed how I approach backend architecture.
Start with the Business
When I build business applications, I try to separate business behaviour from infrastructure.
Infrastructure includes things like:
- CRUD
- Searching
- Pagination
- Auditing
- Multi-tenancy
- Authorization
- Notifications
These are necessary, but they are rarely unique to a specific module.
Whether you're building Products, Customers, Orders or Suppliers, those capabilities are usually identical.
Business logic should be where the application becomes different.
Keep the Architecture Small
Instead of adding more layers, I started removing unnecessary ones.
The architecture now looks something like this:
HTTP Request
│
▼
Controller
│
▼
Business Service
│
▼
BaseService
│
▼
IDataService
│
▼
Database
Each layer has one clear responsibility.
Controllers receive requests.
Business services implement business rules.
The base service provides reusable application behaviour.
The data service handles persistence.
That simple separation has proven easier to maintain than having every service implement the same CRUD operations repeatedly.
Group by Business Capability
Another change I made was grouping services around business capabilities rather than around every model.
For example, instead of creating services simply because a model exists, I group related functionality into services such as:
- Product Service
- Order Service
- Inventory Service
- Customer Service
Supporting models don't automatically require their own service unless they introduce their own business behaviour.
That keeps the solution easier to navigate and reduces unnecessary abstractions.
Searching Shouldn't Require New Endpoints
Search requirements tend to grow throughout the lifetime of an application.
It often starts with a few simple endpoints and gradually expands into dozens of variations.
Rather than creating a new endpoint every time another filter is needed, I prefer allowing the client to describe the search.
The backend remains responsible for validating those criteria and translating them into efficient database queries.
The API surface stays stable while search capabilities continue to grow.
Make Infrastructure Automatic
Another principle I try to follow is making infrastructure happen automatically whenever possible.
For example:
- Tenant filtering
- Audit information
- Pagination
- Index creation
- Timestamps
Developers shouldn't need to remember these concerns throughout the application.
The framework should handle them consistently.
That not only reduces repetitive code but also helps prevent subtle bugs caused by inconsistent implementations.
Why Not GraphQL or gRPC?
GraphQL and gRPC are both excellent technologies.
The choice depends on the problem you're solving.
GraphQL shines when clients need fine-grained control over the shape of the data they retrieve.
gRPC excels at high-performance communication between services.
Many business applications, however, primarily need predictable APIs with flexible searching, strong authorization, auditing and maintainable business logic.
In those cases, a well-designed REST architecture with reusable infrastructure often provides everything required while remaining straightforward to develop, test and maintain.
The objective isn't to avoid newer technologies.
It's to choose the simplest architecture that solves the problem well.
Final Thoughts
One lesson I've learned is that complexity rarely arrives all at once.
It accumulates.
A duplicated CRUD method here.
Another search endpoint there.
A new authorization implementation in another module.
Individually, they don't seem significant.
Collectively, they become expensive to maintain.
Good architecture isn't about adding more layers or adopting every new technology.
It's about removing unnecessary repetition so developers can focus on solving business problems.
That's the principle I now try to apply whenever I design a new business application.
What architectural decisions have helped you reduce repetitive code in your own backend applications? I'd be interested to hear the patterns that have worked well for you.
Top comments (0)