r/odinlang 23d ago

DDD or similar in Odin

Learning Odin coming from Go with profession experience writing in TCL, Python, JS/TS and some C# and loving it. Think Odin is quickly becoming my favourite language to work with.

But I have a question about anyone is doing Domain Driven Development successfully in Odin in large codebases? Is large system adoption not at the level around Odin yet for anyone to try it or care?

Edit: the part I’m interested in with Odin is code isolation so that a core logic (the domain) can be run without any real external dependencies (databases, external rest APIs, the file system etc) at scale. My current main work codebase is around 35,000-40,000 lines of Go (I think the domain code is under 10,000 lines). Lots of the coding issues my team hits look to be easier to solve in Odin. As a PoC I would like to write part of the domain logic in Odin and fake the external dependencies providing proof that Odin could be a valid language option within the business and our team. Needs to follow basic DDD as we often have changing external systems we depend on and we aim to implement them without changing domain code where we can.

Upvotes

12 comments sorted by

u/alzareon 23d ago

My take is that DDD is relevant for defining system boundaries and how software created by different teams should interoperate. It is not that relevant at the programming language level. DDD by Evans uses OO because it was in vogue at the time, but the principles don’t require it. In Odin and other systems programming languages, you can use an API with a shared library whereas in web development a web API would be more common, but it’s the functions, the signatures, the names that are exposed in the API that are the most important.
In general though, an API that just exposes functions you can call is the most simple and flexible. Accept plain data and return plain data, don’t force the user too much having to create inputs that have to be a specific parochial datatype.

u/squat001 22d ago

DDD is used for defining internal system boundaries not just inter systems. From a software architecture perspective the desire is to separate the core/business logic code (the domain) from the rest of the code. The domain code should stand alone to allow for testing of this logic in isolation.

The classic abstraction is the database, a repository. The domain code should not rely of the database schema so when querying a database repository the domain code expects the response to be a domain data type, as if we returned a struct define in the database repository code. These typically leads to a set of port and adaptor functions though this just a naming term which personally only loosely follow.

While DDD was developed around OOP does not rely on a programming paradigm at all, what is needed is the ability to separate and run select code in isolation to allow for easy testing.

u/dustandsepia 22d ago

As far as I can tell there’s 2 ways to go about it in a language like Odin.

The blessed path is likely module based encapsulation. Your API or domain level “objects” can be structs and public functions, and any encapsulation has to be achieved with private submodules.

The second way to go about it would be to effectively do OOP with vtables in the same way you would go about it in C or Zig for example.

u/ar_xiv 23d ago

The main issue with modeling large systems might be in naming procedures and data structures. Basically it comes down to how granular and class/method-like you want them to be. You could use packages, or this could just mean a lot of pseudo name spacing with underscores, or making procedures with switches for various types of objects that might be in a union or just have an enum type. I’d say get comfortable with directly mutating objects in procedures via passing pointers. I think at the end of the day, after some broad categorization of your program, I would still recommend Casey Muratori’s “semantic compression” rather than trying to pre-define all your procedures and data structures.

u/squat001 22d ago

Thanks. Been looking at passing procedures to set dependencies, will take a look at Casey Muratori’s “semantic compression”. A quick glance and looks to be an interesting idea.

u/sudo-maxime 22d ago edited 22d ago

EDIT: rewritten for spelling / precision.

Hey, I’ve been down this rabbit hole while moving from enterprise applications toward systems-level programming. The important thing is to separate the architectural goal from the particular OOP patterns commonly used to achieve it.

What you describe does not necessarily require entities, repository interfaces, or layers of vtables. Start with a use case such as create_user: a procedure with explicit input and output types. Keep the business decisions inside that procedure or in pure domain procedures it calls.

When the use case needs an external capability, pass in the narrowest capability it needs. For example, create_user might require user_exists_with_email, insert_user, and send_welcome_email. This is still dependency inversion, but the abstraction is owned by the use case rather than being a generic repository or I/O interface.

In Odin, that dependency can be represented by concrete procedures selected for the build, by polymorphic code specialized at compile time, or by a small struct of typed procedure pointers when runtime substitution is genuinely necessary. The first approaches can avoid indirect calls; the last is effectively a deliberately small vtable.

I would avoid a universal I/O abstraction with generic save, update, and delete operations. It tends either to grow into a god interface or to leak storage representations back into the domain. Prefer small, use-case-specific ports.

Assertions are useful for validating runtime preconditions—for example, ensuring an optional callback is non-nil—but they should complement Odin’s type checking rather than replace it. Your adapter’s procedure signatures should still be checked by the compiler.

For testing, provide a small in-memory implementation of the same capabilities, but make sure its semantics resemble the real adapter. Otherwise, an overly permissive mock can make tests pass while the PostgreSQL or filesystem implementation still fails.

This gives you the isolation you want without attempting to reproduce a Java-style DDD architecture in Odin.

Example

create_user :: proc(
  input: Create_User_Input,
  port: Create_User_Port,
) -> (
  output: Create_User_Output,
  err: Create_User_Error,
) {
  assert(port.state != nil)
  assert(port.email_is_taken != nil)
  assert(port.insert_user != nil)

  defer {
    if err == .None {
      assert(output.id != User_Id(0))
    } else {
      assert(output.id == User_Id(0))
    }
  }

  if len(input.name) == 0 {
    return Create_User_Output{}, .Invalid_Name
  }

  if len(input.email) == 0 {
    return Create_User_Output{}, .Invalid_Email
  }

  taken, storage_err := port.email_is_taken(
    port.state,
    input.email,
  )

  if storage_err != .None {
    return Create_User_Output{}, .Storage_Failure
  }

  if taken {
    return Create_User_Output{}, .Email_Already_Used
  }

  id, storage_err := port.insert_user(
    port.state,
    input.name,
    input.email,
  )

  if storage_err != .None {
    return Create_User_Output{}, .Storage_Failure
  }

  // The insert adapter promised this.
  assert(id != User_Id(0))

  return Create_User_Output{id = id}, .None
}

u/squat001 21d ago

Thanks, I think this is the direction I want/need. I was going to prototype it today but some actual work got in the way.

u/spyingwind 23d ago

DDD kind of relies on OOP, encapsulation, polymorphism, and dynamic runtimes. Some of these you can simulate in Odin, but not to the extent that DDD likely needs.

You can so some of this with packages and unions a little bit.

What you could do is write a language in Odin that is designed for DDD. That Odin would excel at.

u/squat001 22d ago

While DDD was developed around OOP does not rely on a programming paradigm at all.

In software architecture it’s about code isolation, seen this being done with functional codebases where adaptor methods are passed down a functional flow to allow for faking/mocking interaction with an external system. In the core, this made the main execution code horrible but it did keep the domain code clean and easy to test and allowed external dependencies to be swapped out without writing a single change in the core logic.

This is the power of DDD at the software architecture level (IMO), also come in other forms and names when not used in a wider DDD system, which is more corporate focused (I have seen open source projects use it well though). It’s not needed for every project but large projects with lots of external communication requirements or that need to adapt to integration changes.

u/Imaginos_In_Disguise 22d ago edited 22d ago

You'd do it the same way you'd do it in any language, including C: using dynamic dispatch interfaces (vtables, which are basically structs of function pointers) to decouple business logic from concrete implementation.

In languages like C, Zig and Odin you don't get a language-level construct to build the vtable implicitly for you, so you have to do it explicitly.

A small example as to how you could structure it to mimick what a Rust dyn Trait or C++ virtual class would do:

Animal_VTable :: struct {
    sound: proc(ptr: rawptr) -> string,
}

Animal_Impl :: struct {
    ptr:    rawptr,
    vtable: ^Animal_VTable,
}

Dog :: struct {}

dog_sound :: proc(dog: rawptr) -> string {
    return "bark"
}

dog_animal_vtable := Animal_VTable {
    sound = dog_sound,
}

dog_animal :: proc(dog: ^Dog) -> Animal_Impl {
    return Animal_Impl{ptr = dog, vtable = &dog_animal_vtable}
}

Cat :: struct {}

cat_sound :: proc(cat: rawptr) -> string {
    return "meow"
}

cat_animal_vtable := Animal_VTable {
    sound = cat_sound,
}

cat_animal :: proc(cat: ^Cat) -> Animal_Impl {
    return Animal_Impl{ptr = cat, vtable = &cat_animal_vtable}
}

main :: proc() {
    dog := Dog{}
    cat := Cat{}
    animals := []Animal_Impl{dog_animal(&dog), cat_animal(&cat)}

    for animal in animals {
        fmt.printf("%s\n", animal.vtable.sound(animal.ptr))
    }
}

u/FancierHat 22d ago

And odin already has a few places where the core library and runtime do things like this. Allocators are essentially this exact idea, you pass a set of functions for the runtime to use when allocating data.

u/Bahatur 21d ago

Answering some of your questions more directly: I don’t know of anyone doing DDD in large Odin codebases.

My expectation is that they wouldn’t be, for the traditional enterprise-but-not-1.0 reasons. The good news there is Odin 2027 has been announced, so your timing is good! In terms of where you could look for how to approach the DDD question:

First, the commercial tags in the Odin showcase, especially from JangaFX, where gingerbill works. I don’t actually know about how they do development for their products and if it includes DDD, but they are big enough to be eligible: https://odin-lang.org/showcase/

Second, though I am not familiar with DDD, a surface-level pass suggests to me that you might get relevant insight out of hot code reloading in Odin. Note it is much lower-level, but shares directional goals: https://zylinski.se/posts/hot-reload-gameplay-code/