A habit is a behavior to track, identified by a name and a target number of times per week. For example, “practice Go” with a weekly frequency of five describes an objective, not five completed sessions. Creating, listing, ticking, and checking habits turns that objective into a weekly tracker. section 10.1.1 section 10.7
Listing 10.3 habit.proto: Defining the Habit message
// Habit represents an objective one wants to complete
// a given number of times per week.
message Habit { #1
// Name of the habit, cannot be empty
string name = 1; #2
// Frequency, expressed in times per week.
int32 weekly_frequency = 2;
}
The proto3 Habit message defines the shared API fields: a nonempty name and a weekly frequency measured in times per week. section 10.1.1
Keep the business entity separate from the Protobuf message. The Go domain type adds an ID for finding the habit again and a creation time, neither of which comes from the creation input. Distinct ID, Name, and WeeklyFrequency types also make arguments harder to confuse. section 10.3.1
Listing 10.13 internal/habit/create.go: Validate/complete entity
// validateAndCompleteHabit fills the habit with values that we want in
// our database.
// Returns InvalidInputError. #1
func validateAndCompleteHabit(h Habit) (Habit, error) {
// name cannot be empty
h.Name = Name(strings.TrimSpace(string(h.Name))) #2
if h.Name == "" {
return Habit{}, InvalidInputError{ #3
field: "name", reason: "cannot be empty"}
}
if h.WeeklyFrequency == 0 {
h.WeeklyFrequency = 1 #4
}
if h.ID == "" {
h.ID = ID(uuid.NewString()) #5
}
if h.CreationTime.Equal(time.Time{}) {
h.CreationTime = time.Now()
}
return h, nil
}
Before saving a habit, domain validation trims its name, rejects an empty result, defaults zero weekly frequency to one, and fills missing ID and creation time. Keeping this work in the domain also protects callers that bypass the gRPC API. section 10.3.1
A tick records an achievement; it is not another field on the habit. Ticks are stored separately so a list of objectives and a habit’s status for a week can be retrieved independently. TickHabit uses the habit ID, while GetHabitStatus returns the habit with its tick count. section 10.7.1 section 10.7.6
From target to progress
Create “Write some Go code” with weekly_frequency set to 3. Its response supplies an ID. Pass that ID to TickHabit, then call GetHabitStatus: the habit still has a target of three, and ticksCount is one. A timestamp can select another week for ticking and status checks. section 10.7.6
A frontend can use a different Habit shape: ID, name, weekly frequency, and this week’s tick count instead of creation time. A gRPC client translates backend responses into that shape; a Go template can then display each habit’s exposed Name and WeeklyFrequency fields. section 11.1.3 section 11.2.2
