Back

Kanban

A real-time kanban board I built while picking up aggregates, domain modelling and hexagonal architecture at the same time, which is most of why it came out heavier than the problem needed.

I built this to learn. It is not a product and nobody is using it. The point was to take one feature that sounds simple, a board several people edit at the same time, and find out what it actually costs.

Three patterns at once

I was learning aggregates, domain modelling and hexagonal architecture in the same project. That is most of why it came out heavier than it needed to be: each of the three asks you to put a boundary somewhere, and while you are still learning them you draw every boundary the book mentions rather than the ones the code is asking for.

Here is the whole of the task aggregate:

type Task struct {
	ID        string
	Title     string
	Content   *string
	ColumnID  string
	ProjectID string
	CreatedAt time.Time
}

No methods, no invariants, nothing a caller could get wrong. There are no methods anywhere in the domain package, so every rule lives in the services and the handlers instead, which is the thing the pattern exists to prevent. I had the folder layout of a domain model without the model.

The ports have the same shape of problem:

type TaskService interface {
	CreateTask(ctx context.Context, task *domain.Task) error
	GetTaskByID(ctx context.Context, id string) (*domain.Task, error)
	GetTasks(ctx context.Context) ([]*domain.Task, error)
	UpdateTask(ctx context.Context, task *domain.Task) error
	DeleteTask(ctx context.Context, id string) error
}

One implementation, and there will never be a second. What I would keep is the interface at the storage boundary, where a test can swap Postgres for a fake and it pays for itself. What I would drop is a port for every service.

The wider lesson is about fit. A kanban board is mostly create, move and rename; there are few rules that can contradict each other, so there was not much domain to model in the first place. I learned less about DDD here than about when DDD is worth reaching for.

Keeping several browsers in agreement

REST for the writes, a WebSocket hub for telling everyone else. That part was straightforward. What I did not expect was how much of the work is in the ordering: two people dragging the same card, a message arriving before the response to the request that caused it, a client reconnecting and needing to catch up. I handled the easy cases. There is no conflict resolution here beyond last write wins.

Authorization, and where it stops

Projects have teams and roles: owner, admin, write, read. On the REST side a middleware resolves the membership and checks the role before the handler runs. The socket checks too, but only once, when the connection opens:

if projectID != "" {
	_, err = middlewares.CheckAccess(user.ID, projectID, c.Request.Context(), hub.projectMemberService)
	if err != nil {
		return
	}
}

After that the read loop authorizes nothing. A role that changes mid-session, or a client sending something its role does not allow, goes through. That is the gap I would close first, and finding it is what taught me the actual lesson: the socket is not a side channel, and a check on connect is not the same as a check per message.

What I would do differently

Learn one pattern per project. Settle the real-time contract up front instead of adding message types one at a time as I needed them. And cut the scope: teams, roles and invitations are a lot of surface for a project whose actual question was how to keep one board in sync.