An HTTP service for storing, retrieving, and deleting objects organized by buckets.
- PUT / GET / DELETE at
/objects/{bucket}/{objectID} - Per-bucket content deduplication via SHA-256 hashing
- Concurrent-safe in-memory storage with RWMutex
- Configurable port via
--portflag - Graceful shutdown on SIGINT/SIGTERM
- Go 1.22+
- net/http - HTTP server and routing
- encoding/json - JSON responses
- crypto/sha256 + encoding/hex - deduplication hashing
- sync - RWMutex for safe concurrent access
- flag or os.Getenv - configurable port
- log/slog (Go 1.21+) - structured logging
make run
go run main.go --port=8080
# Runs all tests with race detector
make test
# Or directly with go in the root directory
go test ./... -race -v
# Test PUT/GET/DELETE
# In terminal 1:
go run main.go
# In terminal 2 run each curl command
curl -X PUT http://localhost:8080/objects/bucket1/obj1 -d "hello world" -v
curl -X GET http://localhost:8080/objects/bucket1/obj1 -v
curl -X GET http://localhost:8080/objects/bucket1/missing -v # expect a 400 error
curl -X DELETE http://localhost:8080/objects/bucket1/obj1 -v
curl -X DELETE http://localhost:8080/objects/bucket1/missing -v # expect a 400 error
| Method | Endpoint | Request Body | Success Response | Not Found |
|---|---|---|---|---|
PUT |
/objects/{bucket}/{objectID} |
Raw text body | 201 Created + {"id": "<objectID>"} |
N/A |
GET |
/objects/{bucket}/{objectID} |
None | 200 OK + object body |
400 Bad Request |
DELETE |
/objects/{bucket}/{objectID} |
None | 200 OK |
400 Bad Request |
- Deduplication: Objects are deduplicated per bucket using SHA-256 content hashing. Two objectIDs pointing to identical content share one stored copy. Content is garbage-collected when the last referencing objectID is deleted.
The provided API specification prescribes 400 for object-not-found responses
on both GET and DELETE:
Response if object is not found: Status 400 Not Found
This deviates from standard HTTP semantics, where 404 Not Found is the correct
status code for a resource that does not exist. 400 Bad Request conventionally
indicates a malformed or invalid request from the client - not an absent resource.
This implementation follows the spec as written and returns 400.
In a production codebase - this would be raised as a design review item before
implementation the distinction matters for API consumers, monitoring systems,
and anyone writing client-side error handling logic that branches on status codes.
A 400 response would typically prompt a client to fix its request; a 404 signals
the resource simply doesn't exist yet and may be retried or handled differently.
Go is not my primary production language Python is, having used it across Oracle, Twilio, and Cisco. I chose Go for this task deliberately to align with the team's primary language and to begin building production-level Go experience. I used GitHub Copilot to validate idiomatic Go patterns (error handling, RWMutex usage, stdlib HTTP routing) and cross-referenced the official Go documentation throughout. All core design decisions, the deduplication. model, two-level locking strategy, and DELETE garbage collection, were my own.
Tools used: Perplexity AI, GitHub Copilot (VSCode inline suggestions)
Where AI assisted:
-
Boilerplate generation: Copilot suggested initial struct field layouts for
bucketandStoretypes. I reviewed and restructured the locking model, the suggestion used a single global mutex; I changed it to two-level locking (store-level for bucket map access, bucket-level for object map access) to reduce contention. -
Test case scaffolding: Copilot generated the initial
TestPutAndGetfunction signature and table. I added the deduplication tests and the cross-bucket isolation test myself, as these were not suggested and represent the core requirement. -
Error handling patterns: Copilot suggested
errors.Is()usage in handlers. I accepted this as it aligns with Go idioms and my own practice.
Perplexity AI:
- Used for research and architectural guidance during the design phase, specifically to understand idiomatic Go project structure, two-level RWMutex locking patterns for concurrent map access, and SHA-256 based content deduplication approaches.
- Used to clarify Go module path resolution and standard library HTTP
routing conventions (Go 1.22+
net/httpmethod-based routing). - All design decisions, the two-map deduplication model, DELETE garbage collection logic, and graceful shutdown implementation — were my own, informed by the research but not generated by it.
GitHub Copilot:
- Provided inline suggestions for struct boilerplate and test scaffolding.
- Accepted selectively - locking model and deduplication logic were restructured from suggestions based on my own design reasoning.
- All suggestions validated by running
go test ./... -raceand manual code review.
Where I did not use AI:
- The deduplication design (two-map approach: objectID -> hash, hash -> body) was my own design decision after reasoning through the GC implications of DELETE.
- The graceful shutdown implementation was written from memory based on prior production experience.
- All test assertions were written manually.
How I validated AI output:
- Reviewed every Copilot suggestion before accepting, no blind tab-completions.
- Ran
go test ./... -raceafter each significant change to verify correctness and absence of data races. - Read the generated code against the spec line-by-line for the handler logic