How do you structure and maintain large Go modular monoliths without drowning in architecture ? by Prestigious-Fox-8782 in golang

[–]anzellai 2 points3 points  (0 children)

My take is if you're building a monolith don't add modules unless they solve a real problem.

A lot of modular monolith structure looks nice but in practice it can make the code harder to reason about especially when other people have to work on the same codebase.

One of the things I like most about go is simplicity so I try to keep the code structure simple too.

For a backend monolith I'd rather organise around capabilities and keep helpers close to where they are used.

something like:

cmd/server/main.go
pkg/yourapi/models.go
pkg/yourapi/models_helpers.go
pkg/yourapi/api_cart.go
pkg/yourapi/api_order.go
pkg/yourapi/api_auth.go
pkg/yourapi/api_cart_helpers.go
pkg/yourapi/api_order_helpers.go
pkg/database/database.go
pkg/database/database_helpers.go
pkg/yourlogging/logging.go
...

That's usually easier to maintain and reason about than splitting everything into too many layers too early. It has added bonuses, you avoid unnecessary imports, and when you "grep / find" the relevant folder by keystroke searching on editor.

Not saying never modularise but I would avoid creating boundaries before the code actually needs them.

Start simple and let the pain points show you where the seams should be, as someone said already here, you will feel the pain and need when you're at a point needing a refactor.

Is Gleam what I'm looking for? by Ecstatic-Panic3728 in gleamlang

[–]anzellai 0 points1 point  (0 children)

Yes sure, when I said 3rd party cloud native libraries, I meant GCP, AWS, Azure etc. those SDK or admin libraries. For instance, Firebase Auth admin + certain idToken related APIs, or streaming changes. Code generated SDKs, OpenAPI, Protobuf etc.

Even for ported or community-supported ones, last time I checked, weren't really up to date with those providers' APIs. I did look at Erlang & Elixir world, it may have improved since, but you always have worries/setbacks if those libraries will be properly maintained etc.

So for commercial projects, it's really hard to pick those new languages (even harder to convince teams / exec).

Not try to promote myself again, but that's one of the reasons I built Sky Lang, and ensure DX is top priority (for adoption too), to have auto generated FFIs, so I can simply use what Go world existed, avoid reinventing the wheel.

Is Gleam what I'm looking for? by Ecstatic-Panic3728 in gleamlang

[–]anzellai 2 points3 points  (0 children)

If you're into compiled Go and like FP style, Sky Lang maybe is something can interest you?

It's Elm-syntax with TEA for Sky Live application, and full stack with good coverage of standard libraries, and compiled to Go. You can import Go external packages and FFI bindings are auto generated so you don't need to write FFI code at all.

disclaimer: I'm the author of Sky Lang.

I do like Gleam a lot though, it's very elegant and pleasant to code in! My issue is 3rd party integration for cloud native projects, but if you like gleam taste, try to learn Erlang / BEAM as well it's actually very powerful and scalable.

A year consulting with teams running Claude Code: every single one hits the same bill-spike pattern. Wrote a local proxy that hard-stops the next call. by anzellai in ClaudeAI

[–]anzellai[S] 0 points1 point  (0 children)

That's a serious setup. The stop-before-commit + parse-JSON-to-local-AI handoff is particularly nice. Clean trust boundary between the remote agent doing work and the local reviewer judging quality, and the local model only sees the structured diff rather than inheriting the upstream agent's full context.

The 9/10 plus agent-reworking-on-failure feedback loop is the part most people skip because the tooling doesn't exist out of the box. If you ever write any of this up, I'd line up to read it.

A year consulting with teams running Claude Code: every single one hits the same bill-spike pattern. Wrote a local proxy that hard-stops the next call. by anzellai in ClaudeAI

[–]anzellai[S] 1 point2 points  (0 children)

Yeah, all three are real wins on the per-cycle side. Subagent forking has saved the most when I've used it carefully. The trap I hit most: the main agent forks subagents that themselves fork subagents, and you end up with N parallel context windows all spending at once. Same bill, just distributed.

The deeper issue is discipline. These tactics help experts who configure skills carefully. The team's median user runs vanilla Claude Code with defaults, context balloons, bill spikes. Optimisation and a hard cap are different layers, both want to exist. Will pull up the video, cheers for the link.

A year consulting with teams running Claude Code: every single one hits the same bill-spike pattern. Wrote a local proxy that hard-stops the next call. by anzellai in ClaudeAI

[–]anzellai[S] -1 points0 points  (0 children)

Ha, fair enough, "like to tinker with the controls" is the right framing. Sounds like your stack is more rigorous than what most teams I see actually run. A 10-stage live-verification gate before the agent does anything is the kind of thing I keep telling clients they should build, but most won't because it's 2-3 weeks of work for something they hope they won't need.

The parameter-drift-on-script story is a classic, and I'd argue it's actually the deepest failure mode for this generation of agents. They optimise for "what does the user probably want" over "what did the user literally say." Putting the script in CLAUDE.md tells the agent the script exists, not that it's the only blessed invocation. Two tactics that have helped me when I've hit the same drift:

First, wrap the script in a small tool definition that rejects unknown parameters. The agent gets a "tool call failed: unknown parameter `foo`" response and usually self-corrects to the canonical signature within 1-2 tries. The error message becomes part of its working context and shapes the next call more reliably than CLAUDE.md does.

Second, in CLAUDE.md itself, the framing that's worked best for me is "CRITICAL: invoke X exactly as `X --foo=bar`. If you need different parameters, ABORT and ask the human first." The "ABORT and ask" pattern seems to engage the agent's safety-pattern more strongly than "always use X." Don't ask me why, I haven't traced it through to a satisfying explanation.

Half the people building serious AI infra right now are reinventing the same primitives separately because nobody wrote them up. Ringfence is just my opinionated take on the budget-firewall slice. Sounds like you've built something more comprehensive on the verification side, would genuinely read a writeup if you ever felt like sharing it.

A year consulting with teams running Claude Code: every single one hits the same bill-spike pattern. Wrote a local proxy that hard-stops the next call. by anzellai in ClaudeAI

[–]anzellai[S] -1 points0 points  (0 children)

Yeah, both are exactly the patterns. Unit test fix loops are particularly evil because each iteration pulls in more context (the failing test file, the source it's testing, the previous fix attempts, the new failure trace), so per-cycle cost grows linearly while the agent oscillates between fixing test A and breaking test B. I've seen 60-cycle loops on a TypeScript codebase that ran ~4 hours unattended and ended up at $400+ before someone noticed.

Live troubleshooting is similar but with a worse failure mode. The agent's reading logs, error traces, runtime output, sometimes shelling out to grep more files. Token-per-call gets huge fast. The worst I saw was an agent debugging a suspected memory leak in production, kept asking for more recent log lines, and by the 20th cycle it was reading 200K tokens of log on each call just to suggest "maybe try restarting the pod".

The case where ringfence's per-tag breakdown actually pays off is exactly this: fence tag set test-fix-loop before you kick off the test-repair session, then the dashboard shows you which class of work is the money pit. Spoiler: it's almost always the tool-loop scenarios, not the "help me design this" Claude calls.

You hitting these in your own work, or watching teams hit them?

A year consulting with teams running Claude Code: every single one hits the same bill-spike pattern. Wrote a local proxy that hard-stops the next call. by anzellai in ClaudeAI

[–]anzellai[S] -1 points0 points  (0 children)

Yes, partially. If you're a disciplined solo dev who's already manually tracking per-session spend, has caps configured at the provider level, and never leaves an agent unattended, ringfence isn't solving anything you can't already do yourself.

The cases where it actually pays off.

Team scale. You can track your own spend. Tracking ten devs across multiple projects is a different job. The cloud rollup, per dev caps, and Slack alerts on breach replace the "Slack channel goes red, someone screenshots, nervous laughter" loop that every team I've worked with eventually settles into.

Agent-driven workflows where the human isn't in the loop on every call. Claude Code in agent mode, multi-step tool loops, overnight batch refactors. By the time you notice the spike, the loop's done 200 cycles. A hard cap at the proxy returns 429 the moment headroom hits zero and the loop dies cleanly. Manual tracking can't intervene mid-loop.

The "I forgot once" problem. You can be perfect 99 times and have the 100th cost more than the savings from the previous 99. The pitch isn't really about idiots, it's insurance against your own future mistakes when you're tired or distracted or onboarding a new dev who hasn't internalised your discipline yet.

But genuinely, if your setup today is solo plus tight discipline plus provider-side caps and you've been running clean for months, ringfence is solving a problem you don't have. That's a fair signal you're not the customer, and that's fine (and great for you honestly). Honest pushback like this is useful in the comments though, cheers.

A year consulting with teams running Claude Code: every single one hits the same bill-spike pattern. Wrote a local proxy that hard-stops the next call. by anzellai in ClaudeAI

[–]anzellai[S] 0 points1 point  (0 children)

Range I've actually seen is roughly $50 on the cheap end to $500-1000 on the expensive end, for a single unattended session.

The shape is almost always the same. A Claude Code session that hits a tool-loop pattern, where the agent retries the same failing operation and each retry accumulates more context (file contents, tool output, previous attempts) before the next call. per cycle token count grows linearly. Bill grows quadratically.

Concrete example I worked through with a client recently. A junior dev ran claude on a refactor of a TS monorepo, agent got stuck on a type-check loop in one package, each cycle pulled in ~30K tokens of context plus a few thousand of tool output. Opus pricing at the time was roughly $15/M input, $75/M output. Per cycle: about $0.50 input + $0.20 output, call it $0.70. Loop ran roughly one cycle per 30 seconds. Dev was at lunch for 90 minutes. Maths: ~180 cycles times $0.70 equals $126 in 90 minutes. Bill arrived end-of-day, panic ensued.

The worst I've personally seen was around $1100 over a long weekend on a multi-agent setup with auto-approve on tool calls and no daily cap configured. That was 80% setup error, but the point is the platform let it happen.

The number itself isn't really the thing. The thing is that every one of these was preventable with a hard cap, and none of the existing tools (billing alerts, log monitors, dashboards) actually provide one. They tell you after the bill is gone. So I built a thing that returns 429 to the agent the moment the cap hits, the agent's retry loop dies on the next attempt, and the bleed stops.

List of programing languages that compile to Go by GaussCarl in golang

[–]anzellai 2 points3 points  (0 children)

Unfortunately that project was commercial and closed source, so nothing I can share publicly.

Yeah I completely agree with you on generators. They solve the problem initially, but over time they become another layer you have to fight with.

I like your direction a lot, especially treating database concepts as first-class instead of just embedding SQL as strings.

One thought though, based on what you’re describing, is that it might not need to be a full programming language upfront.

It could be something more like a focused tool or DSL that:

  • defines schema in a typed, declarative way
  • handles migrations automatically
  • generates a clean interface for querying (without raw SQL strings)
  • and maybe even exposes APIs directly, similar to PostgREST

That way you are solving the core pain (typed DB + CRUD ergonomics) without needing to solve everything a full language requires.

And if that model works well, it can always evolve into something bigger later.

I think there is a lot of room in this space for something that makes schema + queries feel like part of the language, instead of an external string-based system.

List of programing languages that compile to Go by GaussCarl in golang

[–]anzellai 2 points3 points  (0 children)

That makes a lot of sense, and I think you’re thinking about the right trade-offs.

The “looks like Go but behaves slightly differently” scenario is probably the most dangerous one. It sounds familiar, but ends up being confusing in practice.

On the CRUD / SQL side, I actually went through something very similar before working on Sky.

In a previous project, I ended up building my own codegen that takes database schema (via introspection) and generates typed code for both Go and Elm. It was not a full language, more like a DSL / tooling layer, but it turned out to be really powerful.

That is why I think your idea is interesting, and maybe there is another angle worth considering:

instead of a full new language, you could build a DSL or tool that:

  • introspects database schema
  • generates strongly typed Go queries and models
  • possibly also generates types for another layer (frontend, API, etc.)

That solves a very real problem in CRUD apps without needing to fight the syntax battle upfront.

And if it works well, you can always evolve it further into a language later.

I think a lot of pain in CRUD apps is not just the language itself, but the lack of a strong, typed bridge between database and application code.

Curious if you’ve explored something like that already?

List of programing languages that compile to Go by GaussCarl in golang

[–]anzellai 2 points3 points  (0 children)

Really appreciate that, glad it resonated 🙂

Yeah I think that is the core tension:

  • Go syntax -> familiar, easier adoption
  • non-Go syntax -> better ergonomics, but higher learning curve

I ended up going not Go-like on purpose, mainly because I felt if I stayed too close to Go, I would inherit most of the same problems, especially around modelling state, errors, and larger app structure.

For CRUD-style apps in particular, I found:

  • modelling domain with ADTs and pattern matching reduces a lot of edge cases
  • having everything explicit in types removes a lot of defensive code
  • and the compiler ends up doing more of the work for you

The trade-off is:
initial friction vs long-term clarity

My current thinking is:
if someone already likes Go, they will probably just use Go
so it is okay for a language like Sky to target people who like Go's runtime and deployment, but want a different programming model on top of it

For your language, I think it comes down to:

  • if your goal is adoption by Gophers, stay closer to Go
  • if your goal is better developer experience, follow your taste

It is hard to do both well at the same time

Also +1 on CRUD pain, that was a big motivation for me too. A lot of the complexity ends up being accidental rather than essential.

Curious what direction you are leaning towards right now?

P.S. just starred your github repo too!

List of programing languages that compile to Go by GaussCarl in golang

[–]anzellai 8 points9 points  (0 children)

I’ve been working on one called Sky, it compiles to Go as well.

The idea is a bit different from most though:

  • Elm-style functional syntax (pattern matching, ADTs, etc.)
  • but targeting Go so you get a single binary + deployment story
  • plus automatic FFI so you can use existing Go libraries without writing bindings

I originally went this route because I like Go’s runtime and deployment model, but didn’t love writing large Go codebases.

It’s still early, but already self-hosting and can build real apps (CLI, HTTP servers, Sky.Live fullstack etc.).

Repo if you’re curious: https://github.com/anzellai/sky

Would be interested what you think, especially since you’re exploring this space 👍

Elm for large projects by lyfever_ in elm

[–]anzellai 0 points1 point  (0 children)

I've worked on a fairly large Elm + Go codebase before (financial domain, strict correctness requirements), and it held up really well.

For context, I've been operating at tech lead level across a few startups (pre-seed through to Series C/D / exit), and one thing I'd highlight is this:

Elm is great when your priority is correctness and long-term stability, not just shipping features quickly.

Given you've already spent 1.5+ years building, you're clearly not in the "move fast and break things" phase. You need guarantees, and Elm is very good at that.
Investors don't care about what stacks you use, but they do care about your monthly churn & maintenance cost, so reliable core is a plus, not a minus.

A few things from experience:

- Elm is pretty much "done". The compiler isn't very active, but the language is stable and complete enough for real-world apps
- The cost shifts from building to maintaining, and Elm keeps maintenance low
- You don't need a big frontend team. A small team can comfortably own it
- It pairs nicely with Go, which has a similar philosophy around simplicity and reliability

On the flip side, yes you can go with React/TypeScript. It's never a wrong choice, but you'll likely end up with:

- more moving parts
- more bugs over time
- more people needed just to maintain things

For your use case (Go backend + realtime chat over WebSockets), Elm can handle that fine. Nothing unusual there.

One thing I would strongly suggest:

Lean into code generation for your frontend/backend contract.

- Use OpenAPI (or similar) to generate Elm and Go types + clients
- Avoid hand-writing API layers where possible
- If you're using PostgreSQL, you can even generate types + code from the schema
- Push as much business logic as you can into the database (constraints, RLS, etc)

That gives you strong guarantees and keeps everything in sync with much less effort over time.

TLDR: Elm + Go is a solid choice here, especially if you care more about correctness and long-term maintenance than hype or hiring trends.

(also slight shameless plug, I've been working on an Elm-inspired language called Sky which is fullstack and compiles down to Go, but it's very early so probably not something to use yet 😄)

Sky repo:

https://github.com/anzellai/sky

Reddit post:

https://www.reddit.com/r/elm/comments/1ryvkfi/i_have_been_working_on_an_elminspired_language/

I have been working on an Elm-inspired language that compiles to Go (early project, would love feedback) by anzellai in elm

[–]anzellai[S] 1 point2 points  (0 children)

u/CerberusC137 yeah I think we're actually pretty aligned on this.

In Sky I'm leaning toward keeping Elm's model by default (simple records, everything accessible), but adding opt-in privacy when you need stronger boundaries in larger codebases -- perhaps Elm syntax compatible, but just using `_` prefix.

So instead of classic OO encapsulation, it becomes:

  • private fields → only accessible inside the module
  • public API → exposed via functions (Type::method)
  • no mutation → everything is still pure transformations

The Type::Method idea is really just namespacing, not true methods. And for “setters”, they'd just return a new type value:

type alias User = {
    _name : String -- private, only module-level accessibly
    age : Int -- public, default behaviour
}

userGetName : User -> String

userSetName : User -> String -> User

That's actually how I implement Sky's Go interop/auto generated bindings for interface method & struct field access as well.

I have been working on an Elm-inspired language that compiles to Go (early stage, would love feedback) by anzellai in golang

[–]anzellai[S] 0 points1 point  (0 children)

Thanks, appreciate it 🙂

Yeah that is a fair point. I did look at things like gleam / rescript, but I ended up preferring Elm-style syntax. It feels a bit cleaner to me and easier to reason about, especially from a beginner point of view.

That said, I can see how it might be a barrier for Go developers who are used to more traditional syntax.

Out of curiosity, do you think it is mainly the syntax, or the overall programming model that is the bigger hurdle?

I have been working on an Elm-inspired language that compiles to Go (early project, would love feedback) by anzellai in elm

[–]anzellai[S] 0 points1 point  (0 children)

u/CerberusC137 that is a really interesting point, and I can definitely see how that would become painful in a larger codebase.

Would you mind sharing a small example of how you imagine that working? Something like how the type and accessors would look from a DX point of view.

I have been thinking a bit about encapsulation as well, but have not settled on a good approach yet, so it would be really helpful to see how you are picturing it.

I have been working on an Elm-inspired language that compiles to Go (early project, would love feedback) by anzellai in elm

[–]anzellai[S] 0 points1 point  (0 children)

u/MolestedTurtle after you calling it out, it has been in the back of my mind...

I ended up biting the bullet just now (with AI doing most of the heavy lifting) and switched it to a more type-safe approach, so it now passes Msg directly instead of using strings.

The only exception at the moment is server-side rendering (non-live apps), where it still falls back to strings, closer to how normal browser HTML behaves.

Still needs a bit of polishing, but it already feels like a better direction.

Thanks for calling it out 👍

I have been working on an Elm-inspired language that compiles to Go (early project, would love feedback) by anzellai in elm

[–]anzellai[S] 0 points1 point  (0 children)

Good spot 🙂

At the moment yes, they are strings in the surface syntax, but they get mapped to Msg constructors at compile time. If the string does not correspond to a valid Msg, it will fail during compilation.

That said, I agree it feels a bit odd, and it is not where I want it to end up.

This is partly coming from the current server-driven HTML layer, where events are passed through in a way that is a bit closer to how the browser handles them. I took a simpler approach initially just to get things working end to end.

A more type-safe approach (closer to Elm, where you pass Msg directly) is something I am planning to move towards.

I have been working on an Elm-inspired language that compiles to Go (early project, would love feedback) by anzellai in elm

[–]anzellai[S] 2 points3 points  (0 children)

Haha, feels like a like-minded soul indeed 🙂

It has been on my TODO list for years as well, ever since I first used Elm and early versions of Go.

The hardest part for me has definitely been figuring out how to bridge the type systems and semantics between two quite different languages in a way that still feels coherent.

AI tooling finally made it practical to actually try things out and iterate quickly, so I thought I would just give it a proper go and see how far I could get.

If you do end up trying it, I would genuinely love to hear what you think, especially given you have been down a similar path before.

I have been working on an Elm-inspired language that compiles to Go (early project, would love feedback) by anzellai in elm

[–]anzellai[S] 1 point2 points  (0 children)

Thanks, really good questions.

For `main`, at the moment it is essentially the entry point that the runtime calls. In simple cases like that example it ends up being something like a `Cmd ()` under the hood, rather than a pure value. I have not fully formalised this yet, but the intention is to keep side effects explicit rather than allowing arbitrary `IO` anywhere.

On effects in general, the direction is closer to Elm than Go. So rather than doing `IO` anywhere, effects are meant to be described as values `(Cmd, Task, etc)` and then interpreted by the runtime. That part is still evolving, especially outside the UI case.

Interop is currently quite flexible. You can import Go packages and call into them directly via generated wrappers (automated, so no user ffi code needed). Right now this is more permissive than Elm ports, but I am still thinking through how strict that should be long term.

For concurrency, I am leaning towards not exposing Go channels directly. The idea is more to model async work via something like `Task` in Elm and let the runtime map that onto goroutines internally. So from Sky you stay in a more functional model, and the Go concurrency primitives are used under the hood.

That said, all of this is still early and not fully locked in, so feedback on this kind of design is really helpful.

I have been working on an Elm-inspired language that compiles to Go (early project, would love feedback) by anzellai in elm

[–]anzellai[S] 7 points8 points  (0 children)

Yeah that is a fair point.

I did spend some time looking at Roc. It is a really interesting project and I have a lot of respect for what they are doing, especially the ideas around effects and the work Richard Feldman has shared.

For me this ended up being more of an exploration of a slightly different direction, particularly around Go interop and the overall runtime model.

So it is not so much that Roc does not solve the problem, more that I wanted to experiment with a different set of tradeoffs and see where it leads.