Go Service Architecture
Layered and clean architecture, the direction of dependencies, the adapter pattern, DTO versus domain entity, and graceful shutdown.
7 questions
JuniorTheoryCommonWhat is a layered architecture and why separate transport, domain, and storage?
What is a layered architecture and why separate transport, domain, and storage?
It splits the app into a transport layer (HTTP/gRPC handlers), a domain layer (business logic), and a storage layer (repositories), with dependencies pointing inward. The domain has no transport or DB imports, so you can swap either edge without touching business rules.
Common mistakes
- ✗Pointing dependencies outward so the domain imports the DB driver, which couples business rules to storage
- ✗Confusing layers with separately deployed services — layers live inside one process
- ✗Putting business logic in the transport handler, leaving the domain layer anaemic
Follow-up questions
- →Where would you place input validation — transport or domain — and why?
- →How does the domain layer call storage without importing the DB package?
MiddleTheoryCommonHow does the architectural style Clean Architecture enforce its dependency rule?
How does the architectural style Clean Architecture enforce its dependency rule?
Clean Architecture arranges code in concentric layers — entities, use cases, adapters, frameworks — under one rule: source-code dependencies point only inward. The domain imports no DB, HTTP, or framework code; outer details implement interfaces the inner layers define, so frameworks become swappable plugins.
Common mistakes
- ✗Confusing Clean Architecture with horizontal three-tier layering — its layers are concentric with an inward dependency rule
- ✗Letting the domain import the database or framework package directly instead of depending on an interface
- ✗Thinking the dependency rule is enforced by folder names rather than by which package imports which
Follow-up questions
- →How does dependency inversion let the domain define the repository interface its outer layer implements?
- →Where do DTOs sit relative to use cases and entities in Clean Architecture?
MiddleDesignCommonA Go service in main wires up three components: a database client, a domain service that runs background work against that database, and an HTTP server whose handlers call the domain service. On SIGINT/SIGTERM you must shut everything down gracefully. In what order do you close the three components, and why does the order matter? Describe how main waits for the termination signal and what happens to in-flight requests if you close the database before the HTTP server.
A Go service in main wires up three components: a database client, a domain service that runs background work against that database, and an HTTP server whose handlers call the domain service. On SIGINT/SIGTERM you must shut everything down gracefully. In what order do you close the three components, and why does the order matter? Describe how main waits for the termination signal and what happens to in-flight requests if you close the database before the HTTP server.
Close in reverse dependency order: HTTP server first (stop accepting new requests and drain in-flight ones), then the domain service (finish background work), then the database last. main blocks on a signal channel registered via signal.Notify(c, SIGINT, SIGTERM) and proceeds on receive. If you close the database first, in-flight handlers and background work suddenly hit a dead connection and return errors to clients — the opposite of graceful.
Common mistakes
- ✗Closing the database first, breaking in-flight handlers and background work
- ✗Thinking order does not matter and closing all components concurrently
- ✗Believing closing the database queues queries instead of failing them
Follow-up questions
- →How does
http.Server.Shutdowndrain in-flight requests before returning? - →Why add a timeout to the whole shutdown so a stuck drain cannot hang forever?
MiddleTheoryOccasionalHow does an adapter or facade isolate code from an external data format?
How does an adapter or facade isolate code from an external data format?
You define a stable internal interface that your code depends on, then write an adapter that wraps the external API or format and implements that interface. When the external side changes, only the adapter is rewritten — callers stay untouched because they only ever see the internal interface.
Common mistakes
- ✗Letting callers depend on the external library's types directly instead of an internal interface
- ✗Thinking copying the external struct into your package isolates you — it still changes when they change
- ✗Putting the adapter logic in the domain layer instead of at the storage or transport edge
Follow-up questions
- →Where does the adapter sit relative to the domain layer?
- →How do you test domain code without the real external dependency?
MiddleTheoryOccasionalWhat is the difference between a DTO and a domain entity?
What is the difference between a DTO and a domain entity?
A DTO is a transport-shaped data carrier — plain fields, no behaviour — used to (de)serialize requests and responses. A domain entity holds business state plus invariants and methods. Keeping them separate stops JSON tags and API shape from leaking into business rules.
Common mistakes
- ✗Reusing one struct as DTO and entity, which leaks JSON tags and API field names into business logic
- ✗Believing a DTO carries behaviour — it is data-only, the entity owns the methods and invariants
- ✗Thinking the DTO and entity must have identical fields — the DTO mirrors the wire shape, not the domain
Follow-up questions
- →Where in a layered app does DTO-to-entity mapping happen?
- →Why should domain invariants not be enforced on the DTO?
SeniorDesignOccasionalDesign a feature-flag system for a fleet of many Go service instances. Operators flip flags (on/off, percentage rollouts, per-segment rules) from a central control plane, and every instance evaluates flags to decide behaviour. Requirements:
- Evaluation is on the hot path of nearly every request, so a check must be cheap and must not make a synchronous network call per request.
- When a flag changes, the new value propagates to all instances quickly, but the whole fleet need not flip in the same instant — define what consistency you actually provide during the change.
- A single user must not flicker between old and new behaviour while instances are still converging.
- The flag service is the source of truth, yet evaluation must keep working with a safe default if it is briefly unreachable.
Cover where evaluation happens, how updates reach each instance, and what consistency guarantee holds fleet-wide mid-change.
Design a feature-flag system for a fleet of many Go service instances. Operators flip flags (on/off, percentage rollouts, per-segment rules) from a central control plane, and every instance evaluates flags to decide behaviour. Requirements: - Evaluation is on the hot path of nearly every request, so a check must be cheap and must not make a synchronous network call per request. - When a flag changes, the new value propagates to all instances quickly, but the whole fleet need not flip in the same instant — define what consistency you actually provide during the change. - A single user must not flicker between old and new behaviour while instances are still converging. - The flag service is the source of truth, yet evaluation must keep working with a safe default if it is briefly unreachable. Cover where evaluation happens, how updates reach each instance, and what consistency guarantee holds fleet-wide mid-change.
Evaluate flags locally against an in-process snapshot, so a check is a cheap map lookup with no network call on the hot path. A flag service is the source of truth; each instance streams updates (or polls with an ETag) and atomically swaps its snapshot. During a change, expect brief eventual consistency across the fleet — make rules deterministic per-user so a given user sees one stable answer even as instances converge.
Common mistakes
- ✗Calling the flag service on every request, adding a network hop and a hard dependency to every hot path
- ✗Expecting a strongly-consistent fleet-wide flip, when streamed snapshots are inherently eventually consistent
- ✗Evaluating randomly per request instead of deterministically per user, so one user flickers between variants
Follow-up questions
- →How do you keep a single user on one variant even though instances update their snapshots at different times?
- →What happens to evaluation if the flag service is unreachable, and what is the safe default?
SeniorDesignOccasionalDesign the public Go API of a client package for a networked key-value store (think a small memcached). Sketch the exported types and the function and method signatures only — no implementation. Satisfy these requirements:
- Initialization takes one required parameter (addr string, e.g. 10.11.0.12:6379) and optional ones (authKey string, connTimeout time.Duration). Adding a new optional parameter later must not break existing callers.
- Avoid a two-step constructor (Create then Init/Connect) and avoid a separate constructor per combination of optional parameters.
- Read and Write must each accept a context.Context so callers can set deadlines and carry tracing.
- Read must disambiguate "key absent" from "key present but the stored value is nil/empty" — decide how the signature expresses that.
Explain your choice for handling optional parameters, why context.Context is the first argument, and how Read resolves the absent-versus-empty ambiguity.
Design the public Go API of a client package for a networked key-value store (think a small memcached). Sketch the exported types and the function and method signatures only — no implementation. Satisfy these requirements:
- Initialization takes one required parameter (addr string, e.g. 10.11.0.12:6379) and optional ones (authKey string, connTimeout time.Duration). Adding a new optional parameter later must not break existing callers.
- Avoid a two-step constructor (Create then Init/Connect) and avoid a separate constructor per combination of optional parameters.
- Read and Write must each accept a context.Context so callers can set deadlines and carry tracing.
- Read must disambiguate "key absent" from "key present but the stored value is nil/empty" — decide how the signature expresses that.
Explain your choice for handling optional parameters, why context.Context is the first argument, and how Read resolves the absent-versus-empty ambiguity.
Use the functional-options pattern: New(addr string, opts ...Option) (*Client, error) with WithAuthKey/WithTimeout options — required parameter positional, optionals extensible without breaking callers, single constructor. Methods take context.Context first by convention, so deadlines/tracing flow in. Resolve nil-vs-absent with an explicit signal: Read(ctx, key) ([]byte, bool, error) (a found flag) or a sentinel ErrNotFound.
Common mistakes
- ✗A two-step Create+Init or a constructor per option combination instead of options
- ✗Passing a raw
time.Durationdeadline instead of acontext.Context - ✗Returning
nilfor both absent and empty, leaving the cases indistinguishable
Follow-up questions
- →How do functional options stay backward-compatible when you add a new setting?
- →Why prefer a sentinel
ErrNotFoundover afound boolin some APIs?