Statless? Decoupled? Think Again. The Real cost of Combine in Complex system

After exploring Swift’s recommendation to “use struct by default,”
I started thinking about how this impacts data ownership, freshness, and system design — especially in reactive contexts like Combine.

In my previous post (link), I showed that structs can behave like references in terms of performance thanks to Copy-on-Write (COW) — as long as they remain unmutated.
They’re also safer in terms of side effects.
So in a clean architecture, structs are often the best choice.

But let’s go deeper. What are the actual tradeoffs between different passing strategies?

  • struct

  • inout

  • reference

Passing struct

Who is the owner?

The module that created the struct.

What are the adwantages?

  • Stateless

  • No side effects

  • Reference-like performance (COW)

  • Clear SRP-based design

  • Easy to debug (only one entry point)

  • Easy to unit test

  • Thread-safe

  • Scalable

  • Easy to maintain

What are the disadvantages?

  • No freshness — if the data changes, consumers are not notified

  • The creator must guarantee consistency — unsafe if misunderstood

Passing a reference

Who is the owner?

Everyone who can access the object.

What are the adwantages?

  • Data stays fresh

  • Good performance

  • Easy to modify

What are the disadvantages?

  • No clear responsibility

  • Architecture gets muddy

  • Side effects — e.g. race conditions, inconsistent states

  • Not thread-safe

  • Harder to debug

  • Limited scalability

Hybrid inout solution

Who is the owner?

Any module that gets control over it.

What are the adwantages?

  • Data stays fresh

  • Controlled side effects

  • Better separation than shared references

  • Easier to debug than reference, though harder than struct

  • More scalable than reference

What are the disadvantages?

  • COW is broken → worse performance

  • Still has side effects

  • Adds complexity

The Core Tradeoff

You’re always navigating between:

StrategyPerformanceIsolationFreshness
Struct
Reference
Inout✅ (partly)

Combine Enters the Picture

So what if you want both freshness and performance — but without fully giving up isolation?

Let’s see what happens when you use Combine with a struct.

Who is the owner?

Still the creator module.

What are the adwantages?

  • Maintains performance (COW)

  • Can preserve stateless design

  • Easy to debug

  • Thread-safe

  • Unit testable

  • Freshness is solved

  • Scalable and maintainable

What are the disadvantages?

  • Side effects (even if controlled)

  • No true isolation — any module with access can send()

  • Ownership boundaries become blurred

Combine solves freshness.
But it sacrifices ownership and isolation — unless you explicitly design around it.

Now, anyone with access to the subject can mutate the state.
The illusion of statelessness breaks. Side effects sneak into seemingly pure modules.

The data flow becomes decoupled from state — so there are two layers of responsibility:

  • Static snapshot (initial struct)

  • Reactive updates (stream)

StrategyPerformanceIsolationFreshness
Struct
Reference
Inout✅ (partly)
Combine
0
Subscribe to my newsletter

Read articles from Seng Phrakonkham directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Seng Phrakonkham
Seng Phrakonkham