Skip to content
C.W.K.
Stream
Lesson 01 of 05 · published

The Shim Is the App's Half

~12 min · shim, seam, design, implementation

Level 0Loose Parts
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Every Shared Module Has Two Halves

A shared module deliberately does not know its app. Something has to supply the missing knowledge, and that something is the shim: a small file, owned and authored by the app, that binds the shared machinery to this app's constants and wraps it in this app's vocabulary.

Shims are outside the deploy manifest. They are never generated, never checked for drift, and the shared repository has no idea they exist. That asymmetry is the point — the copies are byte-identical everywhere and the bindings are all different, and each half is enforced in the way that suits it.

What Actually Goes in One

Three things, in practice, and no more:

  • Constants the shared code takes as parameters. A key slot name, a surface identifier, a database path. The shared module wrote a parameter precisely so it would not have to know these.
  • Re-exports that preserve the app's existing call sites. When shared machinery replaces hand-written code, the shim can expose the same function names the app already calls, so adoption is a one-file change rather than a hundred-call-site change.
  • Domain wrappers. The app's own functions, expressed in its own language, that happen to call shared machinery underneath.

What must never go in one is a modification of shared behavior. If a shim finds itself reimplementing part of what it wraps, that is the signal that either the shared module needs a parameter it does not have, or the behavior was never shared to begin with.

An adoption that changes every call site will not happen. The single biggest predictor of whether an extraction actually lands is whether a consumer can adopt it by editing one file. A shim that re-exports the app's existing names turns 'rewrite this app to use the shared layer' into 'point this file at the shared layer', and that difference is usually the difference between a migration that finishes and one that is abandoned half-done.

The Shim Is Where Divergence Is Allowed to Live

There is a second, quieter function. When one app genuinely needs something different, the first question is always whether the difference can live in the shim. Often it can — a different key slot, a different surface name, an extra wrapper — and then the shared module never learns that any app is special.

Only when the difference cannot be expressed in a shim does the shared module need to grow a parameter, and only when it cannot be expressed as a parameter does anything harder need to happen. That ordering is worth holding explicitly, because the instinct under time pressure runs the other way: reach into the shared file first, because that is where the code is.

Code

A shim, complete — everything the shared module refused to know·python
"""Encryption for this app: the shared algorithm, bound to this app's
key slot, exposed under the names the app already calls.

App-owned. Not in the deploy manifest, not drift-checked, invisible to
the shared repository.
"""

from . import kit_veil as _kit

# 1. CONSTANTS the shared code takes as parameters. The kit wrote
#    these as arguments precisely so it would never have to know them.
_veil = _kit.Veil(
    service="app-veil",              # where this app's key lives
    account="journal",               # its slot within that service
    env_var="JOURNAL_VEIL_PASSPHRASE",
)

# 2. RE-EXPORTS that preserve existing call sites. Every module in the
#    app already imports these two names from here; adoption of the
#    shared implementation touched exactly this file and nothing else.
encrypt_line = _veil.encrypt_line
maybe_decrypt_line = _veil.maybe_decrypt_line


# 3. DOMAIN WRAPPERS: the app's own vocabulary over shared machinery.
#    Note what this function knows that the kit must not - what a
#    journal entry is, and which of its fields are sensitive.
def seal_entry(entry: dict) -> dict:
    out = dict(entry)
    for field in ("body", "private_note"):
        if out.get(field):
            out[field] = encrypt_line(out[field])
    return out


# WHAT MUST NEVER APPEAR HERE: a reimplementation of shared behavior.
#
#   def encrypt_line(text):            # <- NOT this. If the shared
#       if text.startswith("#"):       #    version needs to skip
#           return text                #    comment lines, that is a
#       return _veil.encrypt_line(text)#    PARAMETER it is missing,
#                                      #    or behavior that was never
#                                      #    shared. Either way the fix
#                                      #    is upstream, not here.

External links

Exercise

Take a utility module you have copied between two projects and write the shim split: which lines are the mechanism (identical in both) and which are the bindings (different in each). Then check whether the binding half can be expressed purely as constructor arguments. If it cannot, you have found the parameter the shared version would need — write it down before you extract anything.
Hint
The bindings are usually easy to spot: they are the lines containing a string literal that names something in your world. Paths, key names, table names, environment variables, surface identifiers. Mechanism lines rarely contain proper nouns at all.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.