DEV Community

Cover image for How a solo dev builds like a team: freeze the seams, not the plan
Dmitry Isaenko
Dmitry Isaenko

Posted on

How a solo dev builds like a team: freeze the seams, not the plan

I build LaraFoundry alone. One person, one branch, one task at a time. A reusable SaaS engine for Laravel, extracted out of a real CRM (Kohana.io) that I am building in public.

Working solo, I never hit the question that breaks most teams: how do two people build two parts of the same system at the same time without stepping on each other.

Then I asked myself that question anyway, just as a thought experiment. And the answer changed how I see my own codebase.

The trap I almost fell into

When you imagine handing a big project to a team, the instinct is obvious. You think: I need a giant document first. Every route, every file name, every method, what it takes in, what it returns, every test and what it should assert, every security rule. One huge map of the whole thing, so two people never collide.

That feels like the grown-up way to do it. It is not. It has a name in the industry, Big Design Up Front, and teams moved away from it on purpose.

Two reasons it fails:

The map rots faster than you can write it. By the time you finish specifying every method of module E, building module A teaches you that three of those methods are wrong and two are missing. You wrote a document just to throw it away.

Detail is not the same as safety. A risky project does not get safer because the plan has more words in it. It gets safer when you test the expensive assumptions early. Writing a method signature is cheap and proves nothing. Building one vertical slice that actually runs through tenancy, billing and auth is expensive and proves everything.

So the giant upfront diagram is not the ideal you failed to reach. It is the thing you were right to skip.

What actually lets two people work in parallel

Teams do not write the giant map. They do three things instead.

They split the system by seams, not by phases. My phases (A, then E) are just one person slicing time. A team slices by module with clear borders. Auth, Tenancy, Billing, Navigation. Each one owned by someone, each one with a small public contract, and total freedom inside. You can parallelize exactly as much as your borders are clean.

They freeze contracts, not implementations. This is the key. To stop team B waiting on team A, you fix the interface between them and nothing else. Not every method of a class. Just the one contract through which A and B talk. An interface plus a DTO. Team B writes against the interface, team A implements it. B does not wait, B mocks the interface and keeps going.

They track dependencies as a graph, not a wall of text. Task E depends on contract C, which A must freeze. Once C is frozen (frozen, not implemented), E can start against a mock. Most of the time you discover that 80 percent of the "dependencies" only need a frozen contract, not a finished implementation, and you can parallelize almost everything.

"Freeze" does not mean carved in stone

Freezing a contract means: from this point, other people build on this shape, and nobody changes it silently and alone.

It is not forbidden to change. The cost just jumped. Before you freeze, change it however you like, it is your draft, nobody leans on it, free. After you freeze, you can still change it, but it is no longer your private decision. You are breaking someone else's work. So there is a protocol: announce it, agree with whoever depends on it, ship it as a versioned change, let everyone migrate.

That is the whole point of a freeze. It is the moment a change stops being an edit in your editor and becomes an event with a protocol. That jump in cost is exactly what gives everyone else permission to build on top of you in peace.

And you freeze the shape, not the guts:

interface PaymentGatewayManager
{
    // Frozen: the name, the inputs, the return type.
    public function charge(Money $amount, Customer $customer): ChargeResult;
}
Enter fullscreen mode Exit fullscreen mode

Frozen: the method is called charge, it takes Money and Customer, it returns a ChargeResult. Not frozen: how it works inside, Stripe or Paddle, how many private methods it has. Team A can rewrite the guts ten times and team B never notices, because the contract did not move. You freeze the narrow border and leave full freedom behind it.

The plot twist on my own code

Here is what hit me. I went looking through LaraFoundry for the parts that would survive a team. And they were already there.

The billing add-on talks to the free core through a contract called EntitlementResolver. Navigation plugs into the app through a MenuProviderInterface. The admin dashboard accepts widgets through a DashboardWidgetProvider. Events carry a fixed payload.

I had been calling these "seams" the whole time. I built the billing add-on entirely against the core's contracts, and the core did not change while I did it (the tag stayed put). That is a freeze in action. The add-on leaned on a shape, the shape held, the add-on shipped on its own track.

So I had stumbled into the exact mechanism a team uses to work in parallel. I just was not using its main superpower, because solo and sequential I never needed to.

This is the honest version of the story. Not "I planned it all perfectly from day one." I did not. My seams were born inside the phase where I first needed them, not frozen ahead of time. The realization is the other way around: the question "how would this survive a team" showed me that the thing I already do for decoupling is the same thing teams do for parallelism.

What I would change the day a second person shows up

Not "write the giant map." This:

Pull the contracts into a frozen layer earlier. Right now my seams appear together with the phase that first needs them. On a team you flip it: one session where you freeze all the cross-module interfaces and event payloads first, with null stubs behind them, then the phases fan out to people.

Write decisions down as records, not as my own memory. Solo, my decisions live in my head and my notes. A team needs the same decisions as small versioned records in the repo, so everyone can see why tenancy is fail-closed without asking me.

Mark each dependency as "blocks on a contract" or "blocks on an implementation." The first unblocks the moment you freeze. The second actually waits. You usually find most of them are the first kind.

How to think about depth of planning

Planning depth is not uniform.

Module borders and the contracts between them: detailed and early. Expensive to change, they block other people.

Module internals: a sketch, details as you go. Cheap to change, they block nobody.

Tests: written as an executable spec, not described in prose. A Pest test is the formal record of what goes in and what comes out. That is why teams do not write "which tests and what they return" in a plan. They write the tests. My core sits on a Pest suite for exactly this reason, the tests are the contract, not the paragraph about the tests.

One picture for it: you plan the doorways between rooms in detail, where they are, how wide, because otherwise the walls will not meet. You do not plan the furniture inside a room. Whoever moves in decides that.

So where did I go wrong

Mostly nowhere. Extract, increment, seams. That is the right shape for a non-trivial project.

The giant upfront schema is an anti-pattern, not an ideal I missed. The secret to parallel work is not "write everything down." It is freeze the narrow contracts at the borders before the implementations diverge, and work against mocks. My seams were that mechanism the whole time. The only thing I would add for a team is to freeze them earlier, and to write down which dependencies are real and which dissolve the moment a contract is frozen.

If you are building solo and wondering whether you are doing it "properly" for a real project: look at your own decoupling points. The interfaces you made just to keep modules apart. Those are your freeze points. You are closer to how a team works than the giant-document instinct would ever get you.

Follow along

Top comments (0)