Signals don't make bad architecture better
Signals are good.
Really.
Fine-grained reactivity, clearer dependency tracking, less indirect subscription mechanics, better local derivability of state — those are real improvements.
But Signals are a reactivity primitive.
Not an architecture.
And that’s exactly where the misunderstanding starts.
A new reactivity model doesn’t fix misplaced responsibilities. It doesn’t prevent DTO leakage. It doesn’t draw layer boundaries. It doesn’t decide who owns state. It doesn’t automatically keep components small.
Anyone who had bad structure before can use Signals to build bad code that reacts very quickly.

The misunderstanding
Section titled “The misunderstanding”New framework features often create a brief phase of collective hope.
Finally, everything gets simpler.
Finally, less boilerplate.
Finally, no more subscription hell.
Finally, no RxJS for every tiny piece of UI state.
And yes: a lot of that is true.
Signals can significantly simplify code. They can make local reactivity easier to understand. They can bring derivations closer to the state. They can keep you from staging half an Observable production for a trivial piece of UI state.
But they don’t answer architecture questions.
They don’t answer:
- Where does business state live?
- Who’s allowed to change it?
- Where does the UI layer end?
- Where do DTOs get translated?
- Where do use cases get orchestrated?
- How do multiple parts of the application react to one business event?
- Which layer knows about infrastructure?
- Which layer knows about presentation?
Signals give you a very good wire.
They don’t tell you where to run it.
What Signals don’t solve
Section titled “What Signals don’t solve”DTO leakage
Section titled “DTO leakage”When an API structure gets written straight into a signal, it’s still in the wrong layer.
type TaskDto = { task_id: string; created_at: string; assigned_to_user_id: string | null;};
@Component(...)export class TaskCardComponent { readonly task = signal<TaskDto | null>(null);}That’s not better just because task is now a signal.
The component still knows API field names.
The template still knows transport details.
created_at is still a string.
assigned_to_user_id: null still has to get interpreted in business terms somewhere.
Signals don’t turn that into a clean boundary.
They just make the wrong boundary reactive.
It’s still better to do this:
type TaskViewModel = { id: string; createdAtLabel: string; assigneeLabel: string;};
function toTaskViewModel(dto: TaskDto): TaskViewModel { return { id: dto.task_id, createdAtLabel: formatDate(dto.created_at), assigneeLabel: dto.assigned_to_user_id ?? 'Unassigned', };}The UI should consume a ViewModel.
Not the API’s transport model.
Business logic in components
Section titled “Business logic in components”A signal inside a component isn’t automatically bad architecture.
Local UI state is completely fine there.
@Component(...)export class OrderDialogComponent { readonly open = signal(false);
show() { this.open.set(true); }
close() { this.open.set(false); }}That’s UI behavior.
It gets problematic when the component owns business workflows.
@Component(...)export class OrderComponent { readonly loading = signal(false); readonly error = signal<string | null>(null);
constructor( private readonly http: HttpClient, private readonly router: Router, private readonly toast: ToastService, ) {}
submitOrder(data: OrderData) { if (data.items.length === 0) { this.error.set('An order with no line items isn\'t allowed'); return; }
const payload = this.mapToPayload(data);
this.loading.set(true);
this.http.post('/api/orders', payload).subscribe({ next: () => { this.toast.success('Order saved'); this.router.navigate(['/confirmation']); }, error: () => { this.error.set('Save failed'); }, complete: () => { this.loading.set(false); }, }); }}The problem isn’t signal().
The problem is that the component suddenly owns everything:
- the business rule
- mapping
- HTTP
- loading state
- error state
- toast
- navigation
- flow control
This was already bad with Observables before.
With Signals, it’s still bad.
Just written with a more modern API.

Missing layer separation
Section titled “Missing layer separation”Signals can show up anywhere:
In components. In services. In stores. In facades. In mappers. In random utility classes where they’d be better off not existing.
That makes them flexible.
And that’s exactly why they’re dangerous.
When a team doesn’t have a clear ownership model for state, questions like these come up quickly:
- Which signal is the source of truth?
- Who’s allowed to call
.set()? - Is the signal local, or business-relevant?
- Is it UI state or application state?
- Why does the same state get mirrored in three places?
- Why does a service have a signal, a component have a signal, and the store too?
At that point you don’t have architecture.
You have distributed reactivity.
@Injectable()export class OrderService { readonly selectedOrderId = signal<string | null>(null);}
@Component(...)export class OrderListComponent { readonly selectedOrderId = signal<string | null>(null);}
@Injectable()export class OrderFacade { readonly selectedOrderId = signal<string | null>(null);}That’s not automatically wrong.
But without a clear rule, it’s suspicious.
Because now the same business idea exists in multiple places.
And eventually someone asks:
Why is the detail view on order 42, but the list has order 17 marked?
The answer usually isn’t: “Signals are bad.”
The answer is:
Nobody decided who owns this state.
Signals without ownership are global variables with extra steps
Section titled “Signals without ownership are global variables with extra steps”A signal is seductively simple.
export const currentUser = signal<User | null>(null);Done.
Import it everywhere. Read it everywhere. Write it everywhere.
What could possibly go wrong?
A lot.
Because that makes currentUser reactive, sure, but not architecturally controlled.
Who’s allowed to set the user? When does it get reset? What happens on logout? What happens on a token refresh? What happens on a tenant switch? Which tests need to prepare for this? Which components depend on it indirectly?
A global signal without ownership isn’t a state management solution.
It’s global state.
With a prettier API.
What Signals make possible
Section titled “What Signals make possible”Signals are strong when they get used within clear boundaries.
For example:
Component consumes ViewModel sends intent
Facade / Store owns state coordinates UI-adjacent flows exposes derived signals
Use Case / Application Service executes business workflows
Infrastructure talks to the API, storage, external systemsThen Signals fit very well.
@Injectable()export class OrderFacade { private readonly store = inject(OrderStore);
readonly vm = this.store.vm; readonly isLoading = this.store.isLoading;
loadOrders() { this.store.loadOrders(); }
submitOrder(data: OrderFormValue) { this.store.submitOrder(data); }}The component stays boring:
@Component(...)export class OrderComponent { private readonly facade = inject(OrderFacade);
readonly vm = this.facade.vm;
submit(data: OrderFormValue) { this.facade.submitOrder(data); }}The component doesn’t know what happens afterward.
It doesn’t know whether an HTTP call happens. It doesn’t know whether a toast appears. It doesn’t know whether navigation happens. It doesn’t know whether a list gets reloaded.
It sends an intent.
That’s its job.

A signal store isn’t a free pass
Section titled “A signal store isn’t a free pass”A signal store can be a very good application facade.
But a store can have bad boundaries too.
When everything lands in one store, you just get a new data heap:
type AppState = { user: User | null; orders: Order[]; selectedOrderId: string | null; dialogOpen: boolean; formDraft: OrderFormValue | null; permissions: string[]; theme: string; toastMessage: string | null; tempFlag: boolean;};That’s not better just because it’s a signal store.
A store needs boundaries.
Better:
AuthStore user permissions
OrdersStore orders selectedOrderId loading error commands
OrderDialogStore local dialog state draft stateNot every piece of state belongs in the same place.
Signals make state easy.
Precisely because of that, you have to decide which state belongs where.
Computed isn’t a domain layer
Section titled “Computed isn’t a domain layer”computed() is great for derivations.
readonly fullName = computed(() => { const user = this.user();
if (!user) { return 'Unknown'; }
return `${user.firstName} ${user.lastName}`;});That’s harmless.
But there’s a limit here too.
When complex business rules suddenly land inside computed(), caution is warranted.
readonly discount = computed(() => { const order = this.order();
if (!order) { return 0; }
if (order.customer.type === 'premium' && order.items.length > 10) { return 0.15; }
if (order.customer.country === 'DE' && order.total > 1000) { return 0.1; }
return 0;});Maybe that’s fine.
But maybe it’s a business rule that shouldn’t live in a UI-adjacent derivation.
The question isn’t:
Am I allowed to use computed?
Of course.
The question is:
Is this a presentation derivation, or a business decision?
Presentation derivations fit well in a store, a facade, or ViewModel mapping.
Business decisions belong in a use case, a domain function, or an application layer.
Effects aren’t a trash can for side effects
Section titled “Effects aren’t a trash can for side effects”effect() isn’t an architecture concept either.
An effect can make sense for reacting to state changes.
But when effects get scattered indiscriminately across components and services, you end up with invisible coupling again.
effect(() => { if (this.saved()) { this.toast.success('Saved'); this.router.navigate(['/orders']); this.reload(); }});That can work.
But you should know exactly why this effect sits right here.
Because effects are reactions.
Reactions need ownership.
Who owns the save flow?
Who decides on navigation?
Who decides on toasts?
Who reloads which resource?
How do you test this?
What happens if a second spot also reacts to saved()?
Without a concept, effect() quickly turns into:
Something else just happens somewhere, I guess.
That’s not architecture.
That’s reactive fog.
Good Signals architecture is boring
Section titled “Good Signals architecture is boring”A good Signals architecture isn’t spectacular.
It looks roughly like this:
DTO inMapper translatesStore holds statecomputed produces ViewModelComponent rendersIntent goes outUse case runsResult updates stateThat’s not new.
That’s the point.
Signals don’t change the basic principles of good frontend architecture.
They just make some implementations more pleasant.
The difference isn’t whether you use signal(), Observable, Subject, Redux, or a signal store.
The difference is whether responsibilities are clear.
A practical rule
Section titled “A practical rule”Signals are well suited for:
- local UI state
- derived state
- ViewModels
- feature stores
- facades
- readable reactive data flows
- fine-grained updates
But Signals don’t automatically solve:
- layer separation
- DTO mapping
- use-case orchestration
- ownership of state
- clean event flows
- business modeling
- testability
- component responsibility
The technical question is:
Can I build this with Signals?
The architectural question is:
Should this state or this decision live here?
The second question matters more.
Conclusion
Section titled “Conclusion”Signals are a good tool.
But they’re not a substitute for architecture.
They improve the implementation of good structures. They don’t rescue bad ones.
Anyone shoving DTOs straight into components still has DTO leakage with Signals.
Anyone writing use cases inside components still has business logic in the UI layer with Signals.
Anyone scattering state without ownership still has uncontrolled state with Signals.
Anyone hiding side effects indiscriminately in effects still has hard-to-follow flows with Signals.
Signals make reactive programming in Angular more pleasant.
But they don’t answer the old, uncomfortable question:
Who’s responsible for what?
The short rule:
Signals make good code more readable. They just make bad architecture visible faster.
Or, a bit more unkindly:
Anyone who was building spaghetti before doesn’t get lasagna with Signals. Just finer noodles.