Stop Optimizing Angular for Reuse — Start Optimizing for Change
Why maintainable Angular architecture is designed for change, not maximum reuse.
Stop Optimizing Angular for Reuse — Start Optimizing for Change
Why maintainable Angular architecture is designed for change, not maximum reuse.

Reusability sounds like good architecture. But when Angular teams optimize every component, service, and abstraction for hypothetical reuse, they often create systems that are harder to change. The best enterprise architecture is not the one with the least duplication — it is the one that makes change predictable.
Reuse Sounds Like the Goal
Angular developers are taught to reuse everything.
Reusable components.
Reusable services.
Reusable utilities.
Reusable directives.
Reusable state.
Reusable abstractions.
And reuse is valuable.
But somewhere along the way, many teams begin treating reuse as the primary measure of good architecture.
They ask:
“How can we make this reusable?”
Before asking:
“How is this likely to change?”
That small difference creates very different architectures.
The Most Reusable Code Is Not Always the Most Maintainable
Imagine three features:
Customers
Orders
Invoices
Each feature needs a table.
The tables look similar.
So the team creates:
UniversalTableComponent
At first, it looks like a success.
One component.
Three consumers.
No duplication.
Then the requirements begin to diverge.
Customers need:
- Row selection
- Customer status
- Bulk activation
Orders need:
- Expandable rows
- Shipping progress
- Cancellation actions
Invoices need:
- Currency formatting
- Payment status
- PDF download
The reusable component starts growing.
The Configuration Object Appears
The first solution is usually more configuration.
interface TableConfig {
selectable?: boolean;
expandable?: boolean;
showActions?: boolean;
enableBulkActions?: boolean;
enableDownload?: boolean;
enableStatus?: boolean;
}
Then more requirements arrive.
interface TableConfig {
selectable?: boolean;
expandable?: boolean;
showActions?: boolean;
enableBulkActions?: boolean;
enableDownload?: boolean;
enableStatus?: boolean;
enableCustomerMode?: boolean;
enableOrderMode?: boolean;
enableInvoiceMode?: boolean;
}
The component is technically reusable.
Architecturally, it is becoming dangerous.
Reuse Often Moves Complexity
This is one of the most important lessons in software architecture.
Removing duplication does not automatically remove complexity.
Sometimes it only moves complexity into an abstraction.
Before:
CustomerTable
OrderTable
InvoiceTable
After:
UniversalTable
│
├── Customer Configuration
├── Order Configuration
└── Invoice Configuration
You removed three implementations.
But now every feature depends on one increasingly complex abstraction.
The duplication disappeared.
The coupling increased.
Ask a Better Question
Instead of asking:
“Can these components be reused?”
Ask:
“Will these components change for the same reasons?”
This question is much more useful.
If CustomerTable and OrderTable look similar today but evolve independently, forcing them into one abstraction creates unnecessary coupling.
Visual similarity is not the same as architectural similarity.
Change Is the Real Cost of Software
Software is rarely expensive because of the code that already exists.
It becomes expensive when requirements change.
A customer workflow changes.
A payment rule changes.
A new permission is introduced.
A backend contract evolves.
A feature needs to work differently in another country.
Architecture determines how far that change spreads.
Good Architecture Contains the Blast Radius
Imagine changing an Order requirement.
In a well-designed architecture:
Order Requirement Change
↓
Orders Feature
The impact remains mostly local.
In a tightly shared architecture:
Order Requirement Change
↓
Shared Component
↓
Customers Invoices Reports
Now a local business change creates global risk.
That is the hidden cost of excessive reuse.
Reuse Creates Relationships
Every time two features share an abstraction, they become connected.
That connection may be valuable.
Or it may become a liability.
Consider:
Customers
↓
SharedBusinessComponent
↑
Orders
Now changing the shared component may require understanding both features.
Add five more consumers and the coordination cost increases.
Reuse reduces duplicate code.
But it can increase the number of teams and features affected by change.
Stable Things Are Good Candidates for Reuse
Not all reuse is bad.
Some concepts are naturally stable.
Examples:
- Button
- Dialog
- Tooltip
- Input
- Date Formatter
- HTTP Error Mapper
- Design Tokens
These abstractions change for similar reasons regardless of which feature uses them.
A button changes because the design system changes.
A tooltip changes because accessibility requirements change.
That is healthy reuse.
Business Concepts Often Evolve Differently
Now consider:
CustomerCard
OrderCard
InvoiceCard
They may all visually look like cards.
But they represent different business concepts.
The Customer team may redesign CustomerCard.
The Orders team may add shipment tracking.
The Billing team may add payment actions.
Their visual similarity today tells you very little about how they will evolve tomorrow.
Reuse Should Follow Stability
A useful architectural principle is:
Reuse what is stable. Isolate what changes independently.
This is much safer than:
“If it looks similar, make it shared.”
Stable abstractions create leverage.
Unstable abstractions create coordination overhead.
Angular Makes Premature Reuse Easy
Angular gives us powerful tools:
- Components
- Directives
- Services
- Pipes
- Libraries
- Dependency Injection
- Signals
Because abstraction is easy, developers often create it too early.
Two similar methods?
Create a utility.
Two similar components?
Create a shared component.
Two similar services?
Create a base service.
But similarity is only one signal.
Change patterns matter more.
The Base Class Trap
Consider this:
abstract class BaseCrudComponent<T> {
load(): void {}
create(): void {}
update(): void {}
delete(): void {}
}
Then:
class CustomerComponent
extends BaseCrudComponent<Customer> {}
class OrderComponent
extends BaseCrudComponent<Order> {}
Initially, it feels elegant.
Then Orders needs approval.
Customers need verification.
Invoices need payment processing.
The base class begins accumulating hooks.
beforeCreate()
afterCreate()
beforeUpdate()
afterUpdate()
canDelete()
transformBeforeSave()
The abstraction becomes a framework inside your application.
Composition Usually Handles Change Better
Instead of forcing unrelated features through one inheritance hierarchy, compose smaller capabilities.
For example:
class CustomerFacade {
private readonly api = inject(CustomerApi);
private readonly permissions = inject(CustomerPermissions);
}
class OrderFacade {
private readonly api = inject(OrderApi);
private readonly workflow = inject(OrderWorkflow);
}
Shared infrastructure can still be reused.
Business behavior remains feature-owned.
This allows each feature to evolve independently.
Don’t Build for Every Possible Future
One of the most expensive phrases in architecture is:
“We might need this later.”
Maybe.
But hypothetical reuse often produces real complexity today.
You create:
- Generic interfaces
- Configuration layers
- Extension points
- Strategy patterns
- Base classes
For requirements that may never arrive.
The result is an architecture designed for imaginary futures instead of current business needs.
Build the Simplest Correct Boundary
Suppose two features contain similar code.
That may be acceptable.
You can keep them separate.
Observe how they evolve.
If a stable pattern emerges, extract it later.
Refactoring toward a proven abstraction is usually safer than designing a speculative abstraction upfront.
The Rule of Three Is Useful — But Not a Law
A common guideline is:
The first implementation teaches you.
The second reveals similarities.
The third helps confirm the pattern.
This doesn’t mean you must always wait for exactly three implementations.
The principle is more important:
Understand the pattern before designing the abstraction.
Do not abstract based only on one example and one imagined future.
Duplication Can Preserve Independence
Consider:
customers/
└── customer-status.mapper.ts
orders/
└── order-status.mapper.ts
Both files may initially contain similar mapping logic.
That duplication may be intentional.
Why?
Because Customer statuses and Order statuses belong to different domains.
They may evolve independently.
Combining them into:
shared/
└── universal-status.mapper.ts
could create a relationship that the business does not actually have.
Not All Duplication Is Equal
There is a difference between:
Knowledge duplication
and
Code duplication.
If the same business rule exists in five places, that is dangerous.
For example:
discount = total * 0.15;
copied throughout the application.
If the discount rule changes, multiple places must change.
That knowledge should have one owner.
But two visually similar components may contain similar markup without representing the same business knowledge.
Removing every repeated line is not the goal.
Protecting important knowledge is.
Optimize for the Reason to Change
This is the question I find most useful:
Why would this code change?
A shared button changes because:
- Design system changes
- Accessibility standards change
A CustomerProfileCard changes because:
- Customer requirements change
An OrderSummaryCard changes because:
- Order requirements change
Even if both cards currently look similar, they change for different reasons.
That is a strong argument for keeping them separate.
Feature Boundaries Are Change Boundaries
A good Angular feature is more than a folder.
It is a boundary around related change.
customers/
├── pages/
├── components/
├── state/
├── data-access/
└── public-api.ts
When Customer requirements change, most changes should remain inside this boundary.
That is what makes feature-based architecture powerful.
It localizes change.
Public APIs Protect Change
Suppose Orders depends on Customer data.
Orders should not depend on:
CustomerStore
CustomerInternalService
CustomerApiClient
Instead, Customers can expose a stable capability:
export interface CustomerReader {
getSummary(id: string): Observable<CustomerSummary>;
}
Now the Customer feature can change internally.
Consumers remain protected.
The goal is not maximum reuse.
The goal is minimizing the number of consumers affected by change.
State Should Also Be Scoped for Change
The same principle applies to state.
A selected tab belongs to a component.
Customer filters may belong to the Customer feature.
Authentication state may belong to the application.
If everything becomes global state, every feature can become dependent on everything else.
State scope should reflect the scope of change.
Keep state as local as possible.
Promote it only when the application genuinely requires broader ownership.
A Practical Example
Imagine three features need date formatting.
This may be a good shared abstraction:
formatDisplayDate(date: Date): string
Why?
Because date presentation may be governed by one application-wide standard.
Now imagine three features need status colors.
Customer:
ACTIVE → Green
SUSPENDED → Red
Order:
SHIPPED → Green
DELAYED → Red
Invoice:
PAID → Green
OVERDUE → Red
They look similar.
But they represent different business meanings.
A universal StatusColorService may create unnecessary coupling.
Sometimes three small mappings are clearer.
How Senior Engineers Think About Reuse
Less experienced architecture often asks:
“How much code can we share?”
More mature architecture asks:
“What should be allowed to change independently?”
That shift changes everything.
It affects:
- Component design
- State management
- Service boundaries
- Feature APIs
- Shared libraries
- Team ownership
Reuse becomes a consequence of stability.
Not the starting goal.
A Practical Decision Framework
Before extracting shared code, ask:
1. Is the duplication representing the same knowledge?
If yes, centralization may be valuable.
2. Will these consumers change for the same reason?
If yes, sharing may be appropriate.
3. Is the abstraction already stable?
If no, waiting may be safer.
4. Will this abstraction create cross-feature coordination?
If yes, consider the long-term cost.
5. Can the implementations evolve independently?
If yes, duplication may actually protect the architecture.
Signs You Have Over-Optimized for Reuse
Watch for:
- Components with dozens of Inputs
- Huge configuration objects
- Generic base classes
- Shared services containing business logic
- Feature flags inside reusable components
- One change affecting unrelated features
- Developers afraid to modify shared code
- Abstractions that require extensive documentation
These are often signs that reuse has become more important than clarity.
The Goal Is Not Zero Duplication
A codebase with zero duplication can still be terrible.
A codebase with some intentional duplication can be extremely maintainable.
The real goals are:
- Clear ownership
- Predictable change
- Small blast radius
- Understandable dependencies
- Independent feature evolution
If a little duplication helps achieve those goals, it may be the correct architectural decision.
Final Thoughts
Reuse is valuable.
But reuse is not the purpose of architecture.
The purpose of architecture is to make software easier to change.
Sometimes reuse helps.
Sometimes it creates coupling.
Sometimes duplication is technical debt.
Sometimes duplication protects independence.
The difference comes down to understanding ownership and change.
So the next time you see two similar Angular components, services, or pieces of logic, don’t immediately ask:
“How can I reuse this?”
Ask:
“Should these things be allowed to change independently?”
That question leads to better abstractions.
Better feature boundaries.
And Angular applications that remain maintainable long after the original architecture diagram is forgotten.
Connect with Me
If you enjoyed this post and would like to stay updated with more content like this, feel free to connect with me on social media:
- Twitter : Follow me on Twitter for quick tips and updates.
- LinkedIn : Connect with me on LinkedIn
- YouTube : Subscribe to my YouTube Channel for video tutorials and live coding sessions.
- Dev.to : Follow me on Dev.to where I share more technical articles and insights.
- WhatsApp : Join my WhatsApp group to get instant notifications and chat about the latest in tech
Email: Email me on dipaksahirav@gmail.com for any questions, collaborations, or just to say hi!
I appreciate your support and look forward to connecting with you!
메타데이터
- post_id
- 34cc406e8b3a
- slug
- stop-optimizing-angular-for-reuse-start-optimizing-for-change-34cc406e8b3a
- url
- https://medium.com/angular-engineering/stop-optimizing-angular-for-reuse-start-optimizing-for-change-34cc406e8b3a
- canonical_url
- https://medium.com/angular-engineering/stop-optimizing-angular-for-reuse-start-optimizing-for-change-34cc406e8b3a
- author_url
- https://medium.com/@dipaksahirav
- status
- ok
- fetched_at
- 2026-07-16 18:03:01