{"id":18928802,"url":"https://github.com/samsze0/lucky-draw-web-service","last_synced_at":"2026-04-10T15:42:51.682Z","repository":{"id":202896530,"uuid":"707541187","full_name":"samsze0/lucky-draw-web-service","owner":"samsze0","description":"A web service for lucky draw","archived":false,"fork":false,"pushed_at":"2023-10-23T06:59:51.000Z","size":730,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-12-31T19:17:24.067Z","etag":null,"topics":["axum","docker","docker-compose","postgresql","redis","rust","sqlx","utoipa"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/samsze0.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-10-20T06:05:19.000Z","updated_at":"2023-11-25T17:12:40.000Z","dependencies_parsed_at":"2023-10-24T13:40:19.873Z","dependency_job_id":null,"html_url":"https://github.com/samsze0/lucky-draw-web-service","commit_stats":null,"previous_names":["artizon-io/vendor-web-service","artizon-io/lucky-draw-web-service","samsze0/lucky-draw-web-service"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samsze0%2Flucky-draw-web-service","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samsze0%2Flucky-draw-web-service/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samsze0%2Flucky-draw-web-service/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samsze0%2Flucky-draw-web-service/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/samsze0","download_url":"https://codeload.github.com/samsze0/lucky-draw-web-service/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239923926,"owners_count":19719225,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["axum","docker","docker-compose","postgresql","redis","rust","sqlx","utoipa"],"created_at":"2024-11-08T11:27:49.983Z","updated_at":"2025-12-30T22:58:24.467Z","avatar_url":"https://github.com/samsze0.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Lucky Draw Web Service\n\nProblem specification:\n- Each customer can draw multiple times during the period of the campaign; however,\nthey are only allowed to draw one time each day\n- Customer must fill in the mobile phone number for reference and SMS\n- There are multiple categories of prizes, for example, $5 Cash Coupon, $2 Cash\nCoupon, and Buy 1 get 1 free coupon. Each category, they have its total quota and\ndaily quota\n\n## Architecture\n\n**Database schema**\n\n![](sys-arch.png)\n\nDetails:\n- A draw will only be assigned a `campaign_coupon_id` if the draw wins a coupon.\n- A campaign can be created by sending a POST request to `/campaign` supply it with the list of coupon types to be created.\n- Campaign coupons are lazily generated: a coupon is generated when a draw wins it.\n\n## Design Decision\n\n**DB schema design**\n\nThe `Campaign_Coupon_Type` table could have been further normalized but I have decided to keep it simple.\n\nThe relationship between `Campaign` and `Coupon_Type` could have been a many-to-many one but that would increase the complexity.\n\nScalability-wise, the table that would need the most scaling is `Draw`, and when it is reaching its capacity it can be partitioned/sharded.\n\n**Caching**\n\nThe user might spam the `/draw` API so that a cache is a necesssity for reducing the DB load. The cache stores the mapping of users and the campaigns which they have enrolled in for the day, e.g.:\n\n```\n\"user-1:enrolled-campaigns:2023-10-01\": [\n    \"campaign-1\",\n    \"campaign-2\"\n]\n```\n\nThe cache will also cache the probability distributions of the campaign coupon types, e.g.:\n\n```\n\"campaign-1-prob-dist\": [\n    \"coupon-type-1:0.1\",\n    \"coupon-type-2:0.2\",\n    \"coupon-type-3:0.5\",\n]\n```\n\nThe reason for this is to avoid having to read the probability distribution from the DB everytime a draw is issued. The server node then carry out the sampling to see if the draw has won any coupons. If so, the server then creates a transaction and do the following:\n\n1. Decrement the quota of the `Campaign_Coupon_Type` entry. This operation would fail if the quota has already reached `0` (because of the constrait `quota \u003e 0`). If the operation fails, the server would return a \"no coupon\" message to the user. Note that this is relying on the important assumption that: **once a particular coupon runs out of quota, the probability of winning other remaining coupons will stay unchanged**\n2. Create a coupon entry in the `Campaign_Coupon` table and associate it to the `draw` entry, and return the information about the `coupon` to the user\n\nIn order to reset the `current_daily_quota` everyday, when the server tries to decrement the quote of the `Campaign_Coupon_Type` entry, it checks if the `last_drawn_date` is equal to the current date. If this isn't the case, `current_daily_quota` will be reset to the value of `daily_quota`. The SQL:\n\n```sql\nupdate campaign_coupon_types\nset last_drawn_date = case\n    when (last_drawn_date is null or last_drawn_date != CURRENT_DATE) then CURRENT_DATE\n    else last_drawn_date\nend,\ncurrent_daily_quota = case\n    when (last_drawn_date is null or last_drawn_date != CURRENT_DATE) then daily_quota - 1\n    else current_daily_quota - 1\nend,\ncurrent_quota = current_quota - 1\nwhere id = $1\nreturning *;\n```\n\n**Web server**\n\nI chose to pair Rust with the `axum` web server framework. I have never tried this stack before so I want to challenge myself a bit. Also `axum` supports concurrent DB connections and comes with a connection pool OOTB.\n\n**Database**\n\nI have chosen to use a relational DB because the data entities can be naturally expressed by tables.\n\nI opted for no ORMs because I worried that an ORM might not support all the necessary features the project needs from the DB, even though ORM can provide nice ergonomic features such as dynamic query building.\n\nI picked the `sqlx` Rust library as the DB driver because it supports compile-time query checking . With compile-time query checking, I get to enjoy some of the benefits that are typically associated with ORMs - namely strong types.\n\n## Setup\n\nPrerequisites:\n- `docker`\n\nRun `docker compose up`\n\nThe swagger / redoc / rapidoc UI will be available at `localhost:8080/swagger-ui`, `localhost:8080/redoc`, and `localhost:8080/rapidoc` respectively\n\n## Example\n\n1. Create a user with POST `/user`\n2. Create a campaign with POST `/campaign`\n3. Draw with POST `/draw`\n4. (If won,) Redeem with POST `/redeem`\n\n## Future work\n\n- Authentication \u0026 authorization policies\n- Infra-as-code for deployment, k8s, etc.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamsze0%2Flucky-draw-web-service","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsamsze0%2Flucky-draw-web-service","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamsze0%2Flucky-draw-web-service/lists"}